Transformers para series de tiempo ¶
🛻 Instalaciones: 🛻
Este notebook utiliza Poetry para la gestión de dependencias. Primero instala Poetry siguiendo las instrucciones de su documentación oficial. Luego ejecuta el siguiente comando para instalar las dependencias necesarias y activar el entorno virtual:
- Bash:
poetry install
eval $(poetry env activate)
- PowerShell:
poetry install
Invoke-Expression (poetry env activate)
✋ Importaciones: ✋
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.stattools import adfuller
import keras
from keras import Input, Model
from keras.layers import (
LayerNormalization,
MultiHeadAttention,
Dropout,
LayerNormalization,
Conv1D,
Dense,
GlobalAveragePooling1D,
Layer,
Embedding,
)
from keras.callbacks import EarlyStopping, History
from keras.losses import MeanSquaredError
from keras.optimizers import Adam
from keras.metrics import MeanAbsoluteError
from sklearn import metrics
from typing import Tuple
from pprint import pprint
import torch
import torch.nn as nn
from transformers import TimesFmModelForPrediction, PatchTSTConfig, PatchTSTForPrediction
from sklearn.preprocessing import StandardScaler
from torch.utils.data import Dataset, DataLoader
from sklearn.metrics import mean_squared_error, mean_absolute_error, root_mean_squared_error
🔧 Configuraciones: 🔧
WINDOW_SIZE = 5
EMMBEDDING_DIM = 8
DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'

| Subtitulo | Transformers en series de tiempo - Análisis de series temporales 2 - FIUBA |
|---|---|
| Descrpción | Estudio sobre la aplicación de Transformers en series de tiempo. |
| Integrantes | - Bruno Masoller (brunomaso1@gmail.com)- Fabricio Denardi (denardifabricio@gmail.com) - Francisco Rassi (franciscorassi@gmail.com) |
1. Introducción a los Transformers ¶
Los Transformers son una arquitectura de red neuronal introducida en el artículo "Attention is All You Need" por Vaswani et al. en 2017. Originalmente diseñados para tareas de procesamiento de lenguaje natural (NLP), los Transformers han demostrado ser altamente efectivos en una variedad de tareas, incluyendo traducción automática, generación de texto y análisis de sentimientos. Esta arquitectura se basa en el mecanismo de atención, que permite a la red enfocarse en diferentes partes de la entrada de manera dinámica, lo que mejora la capacidad de modelar relaciones a largo plazo en los datos. Una representación visual del artículo puede verse en la siguiente figura:
Figura: Arquitectura del Transformer, tomada de "Attention is All You Need" (Vaswani et al., 2017).
Más allá de su éxito en NLP, los Transformers han sido adaptados para otras áreas, como visión por computadora, en el artículo "An Image is Worth 16x16 Words" por Dosovitskiy et al. en 2020, donde se introdujo el Vision Transformer (ViT); en el modelado espacio-temporal, introducido en el artículo "A 3d high-resolution model for fast and accurate global weather forecast" por Bi et al. en 2022; o en reconocimiento de voz, como se describe en el artículo "Speech-Transformer: a no-recurrence sequence-to-sequence model for speech recognition" por Dong et al. en 2018. En series de tiempo, también se han explorado diversas adaptaciones de Transformers.
El objetivo de este trabajo es explorar el uso de Transformers en series de tiempo, centrándonos en su aplicación para la predicción de series temporales financieras. A través de un conjunto de datos clásico, como lo es el precio del Bitcoin, analizaremos el rendimiento de varios tipos de Transformers.
💫 Más información: Esta es una breve introducción, si deseas más información puedes mirar este artículo donde se explica don profundidad varios conceptos: https://arxiv.org/pdf/2304.10557
2. Transformers para series de tiempo ¶
La predicción de series de tiempo enfrenta varios desafíos debido a las características propias de los datos. Usualmente, estos desafíos se asocian con conceptos de estacionaridad, linearidad y naturaleza caótica. Enfoques tradicionales basados en aprendizaje profundo como RNN (Recurrent Neural Networks), LSTMs (Long Short-Term Memory) o GRU (Gated Recurrent Units) procesan los datos de forma secuencial pero son ineficientes para secuencias largas.
Para superar estos desafíos, una linea de investigación moderna se basa en la aplicación de los Transformers a estas secuencias de datos, focalizándose en las ventajas que presentan estas arquitecturas en el procesamiento de datos. Entre lás principales ventajas se encuentran:
- Paralelización: A diferencia de las RNN, los Transformers permiten el procesamiento paralelo de secuencias, lo que acelera el entrenamiento y la inferencia.
- Atención: El mecanismo de atención permite a los Transformers enfocarse en diferentes partes de la secuencia de entrada, capturando relaciones a largo plazo y mejorando la capacidad de modelar dependencias complejas.
- Escalabilidad: Los Transformers son altamente escalables y pueden manejar secuencias de longitud variable, lo que los hace adecuados para una amplia gama de tareas de series de tiempo.
En base al artículo "A Survey on Transformers for Time Series Forecasting" de Wu et al. (2023), una posible taxonomía para clasificar a los Transformers para series de tiempo se puede dividir en dos categorías principales: según su arquitectura y según su aplicación, como puede verse en la siguiente figura:
Figura: Taxonomía de Transformers para series de tiempo según Wu et al. (2023).
Un resumen de esta clasificación es el siguiente:
- Clasificación según modificaciones en la arquitectura:
- Modificaciones en "Positional Encoding".
- Vanilla positional encoding.
- Learnable positional encoding.
- Timestamp encoding
- Modificaciones (optimizaciones) en el módulo de atención.
- Introducción de sesgo espacial (LogTrans y Pyraformer)
- Exploración de las propiedades low-rank de la matriz de atención (Informer y FEDformer)
- Inovación en la arquitectura de la atención
- Arquitecturas jerárquicas
- Modificaciones en "Positional Encoding".
- Clasificación según el tipo de aplicación.
- Predicción
- Predicción de series temporales (Informer, etc.)
- Predicciones temporales-espaciales
- Predicciones de eventos.
- Detección de anomalías
- Clasificación
- Predicción
Varias variantes de Transformers han sido propuestas para abordar estos desafíos, cada una con sus propias innovaciones y optimizaciones. Por ejemplo, según la tarea de predicción, se puede nombrar los artículos de "Enhancing the locality and breaking the memory bottleneck of transformer on time series forecasting" por Li et al. (2019) o "Informer: Beyond efficient transformer for long sequence time-series forecasting" por Zhou et al. (2021), que introducen optimizaciones en el módulo de atención para mejorar la eficiencia y la precisión en la predicción de series temporales. Así mismo, para la detección de anomalías, se puede mencionar el artículo "TranAD: Deep transformer networks for anomaly detection in multivariate time series" por Zheng et al. (2021) o el artículo "Voice2Series: Reprograming acoustic models for time series classification" por Yang et al. (2021) para la tarea de clasificación.
Sin embargo, a pesar de los avances en el uso de Transformers para series de tiempo, aún existen desafíos y limitaciones, especialmente en como efectivamente modelar series temporales complejas para capturar la estacionalidad, como se nombra en el artículo "Robust time series anlaysis and applications: An industrial perspective" por Wen et al. (2022).
En base a esa curiosidad, en las siguientes secciones se explorará el uso de Transformers para series de tiempo, centrándose en su aplicación para la predicción de series temporales financieras. A través de un conjunto de datos clásico, como lo es el precio del Bitcoin, analizaremos el rendimiento de varios tipos de Transformers.
2.1. Embeddings y Positional Encoding ¶
Los Transformers proveen una poderosa arquitectura para el procesamiento de una amplia gama de modalidades de datos, como textos e imágenes (explicados anteriormente). En todos estos casos, los datos en "crudo" (raw data) deben primeramente ser "tokenizados" para que se conviertan en una secuencia de vectores que luego pueden ser procesados por el modelo. Debido a que los Transformers son invariantes a las permutaciones de los datos, es necesario incorporar información de orden en la secuencia de entrada. Para esto, se utiliza el "positional encoding", que agrega información de posición a cada token en la secuencia.
Existen varias formas de "tokenizar" las series de tiempo, entre las cuales se encuentran utilizar mutli-series para representar mejor la información semántica, que al utilizar varios canales, mejora la información semántica local, como se comenta en el paper "A time series is worth 64 words: Long-term forecasting with transformers" por Nie et al. (2023). Otras formas incluyen características (handcrafted features) adicionales, como feriados o días especiales, como el modelo Informer.
En la siguiente sección se muestra un ejemplo para un caso "sin positional encoding" y otro "con positional encoding" como estudio.
3. Caso práctico ¶
Esta sección tiene como objetivo presentar un caso práctico de aplicación de Transformers a series de tiempo. El enfoque principal se centra en explorar la construcción de modelos de predicción de series temporales utilizando la arquitectura Transformer, construídos de forma "manual" utilizando Keras.
Se divide en dos partes, por un lado, se realiza la implementación de un modelo Transformer desde cero, en donde las entradas son simplemente series de tiempo univariadas. Por otro lado, se explora y analiza una codificación posicional alternativa, que permite incorporar información adicional al modelo.
Las capas utilizadas en este caso práctico (como sus enlaces a la documentación oficial) son:
Para este ejemplo, se utilizará el mismo conjunto de datos que se utilizó en la primera versión de la materia (precio del Bitcon obtenido desde Binance), el cual se encuentra disponible en: https://github.com/brunomaso1/uba-mia/tree/mia-ast1/mia-ast1/Trabajo%20final/datasets.
Las métricas obtenidas en ese trabajo fueron las siguientes:
df_metrics = pd.read_csv('resources/previous_metrics_comparison.csv')
df_metrics
| model | mae | mape | rmse | |
|---|---|---|---|---|
| 0 | Naive | 1735.726500 | 1.888647 | 2434.504211 |
| 1 | ARIMA(1,1,35) | 1891.662281 | 2.063588 | 2546.257391 |
| 2 | ARIMA(0,1,0) | 1735.726500 | 1.894494 | 2434.504211 |
| 3 | Prophet | 2582.551322 | 2.871448 | 3621.703076 |
| 4 | XGBoost | 4319.031819 | 4.609317 | 5555.304516 |
| 5 | LSTM | 2432.965561 | 2.645095 | 3192.781096 |
Cargamos el conjunto de datos:
df = pd.read_pickle('resources/BTCUSDT_1D.pkl')
df.head()
| Open | High | Low | Close | Volume | Close Time | Quote Asset Volume | Number of Trades | Taker Buy Base Asset Volume | Taker Buy Quote Asset Volume | Ignore | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Open Time | |||||||||||
| 2024-03-01 | 61130.99 | 63114.23 | 60777.00 | 62387.90 | 47737.93473 | 2024-03-01 23:59:59.999 | 2.956537e+09 | 1947444 | 24195.70252 | 1.498771e+09 | 0 |
| 2024-03-02 | 62387.90 | 62433.19 | 61561.12 | 61987.28 | 25534.73659 | 2024-03-02 23:59:59.999 | 1.582567e+09 | 1641808 | 12691.37721 | 7.865831e+08 | 0 |
| 2024-03-03 | 61987.28 | 63231.88 | 61320.00 | 63113.97 | 28994.90903 | 2024-03-03 23:59:59.999 | 1.804536e+09 | 1992011 | 14905.18600 | 9.278690e+08 | 0 |
| 2024-03-04 | 63113.97 | 68499.00 | 62300.00 | 68245.71 | 84835.16005 | 2024-03-04 23:59:59.999 | 5.568878e+09 | 3887853 | 45319.08640 | 2.974396e+09 | 0 |
| 2024-03-05 | 68245.71 | 69000.00 | 59005.00 | 63724.01 | 132696.78130 | 2024-03-05 23:59:59.999 | 8.674527e+09 | 5310706 | 65991.84526 | 4.318206e+09 | 0 |
En este caso, nos quedamos solo con la columna de "Close" del conjunto de datos, que representa el precio de cierre de BTC:
df = df[['Close']]
df.head()
| Close | |
|---|---|
| Open Time | |
| 2024-03-01 | 62387.90 |
| 2024-03-02 | 61987.28 |
| 2024-03-03 | 63113.97 |
| 2024-03-04 | 68245.71 |
| 2024-03-05 | 63724.01 |
Guardamos una copia:
df.to_csv('resources/BTCUSDT_1D.csv', index=True)
Inicialmente, realizamos un test de estacionaridad, utilizando la prueba de Dickey-Fuller aumentada (ADF). Para ello, generamos una función auxiliar que realice el test:
def test_stationarity(timeseries: pd.DataFrame) -> None:
"""
Realiza la prueba de Dickey-Fuller aumentada (ADF) para evaluar la estacionaridad de una serie temporal.
Args:
timeseries (pd.DataFrame): Serie temporal a evaluar, puede ser un DataFrame o una Serie de pandas.
Prints:
Estadístico ADF, valor p y conclusión sobre la estacionaridad de la serie.
"""
adf_result = adfuller(timeseries)
print("ADF Statistic:", adf_result[0])
print("p-value:", adf_result[1])
if adf_result[1] <= 0.05:
print("La serie es estacionaria (rechazamos H0)")
else:
print("La serie no es estacionaria (no rechazamos H0)")
Realizamos el test:
test_stationarity(df)
ADF Statistic: -1.420856101366877 p-value: 0.5722376131152292 La serie no es estacionaria (no rechazamos H0)
📝 Nota: Se podría realizar otras pruebas como KPSS, no linealidad (BDS), normalidad (Shapiro-Wilks), etc. pero no es el objetivo de este trabajo. Por más información sobre el conjunto de datos, se puede acceder en el siguiente enlace: https://github.com/brunomaso1/uba-mia/blob/mia-ast1/mia-ast1/Trabajo%20final/tp-final.ipynb
Dividimos el conjunto de datos en entrenamiento y test (también convertimos los datos en listas):
train_size = int(len(df) * 0.9)
df_train, df_test = df[:train_size].values, df[train_size:].values
spots_train, spots_test = df_train.tolist(), df_test.tolist()
print(f"Train size: {len(spots_train)}")
print(f"Test size: {len(spots_test)}")
Train size: 356 Test size: 40
Definimos una función auxiliar para crear las secuencias de datos:
def to_sequences(seq_size: int, obs: list) -> Tuple[np.ndarray, np.ndarray]:
"""
Genera secuencias de longitud fija y sus correspondientes etiquetas para problemas de series temporales.
Args:
seq_size (int): Tamaño de la ventana o longitud de cada secuencia.
obs (list): Lista de observaciones (puede ser una lista de listas o valores).
Returns:
tuple[np.ndarray, np.ndarray]:
- x: Array de secuencias de entrada de tamaño (n_samples, seq_size, ...).
- y: Array de valores objetivo correspondientes a cada secuencia.
"""
x = [np.array(obs[i : i + seq_size]) for i in range(len(obs) - seq_size)]
y = [obs[i + seq_size] for i in range(len(obs) - seq_size)]
return np.array(x), np.array(y)
Creamos las secuencias de datos:
X_train, y_train = to_sequences(WINDOW_SIZE, spots_train)
X_test, y_test = to_sequences(WINDOW_SIZE, spots_test)
print(f"X_train shape: {X_train.shape}")
print(f"y_train shape: {y_train.shape}")
print(f"X_test shape: {X_test.shape}")
print(f"y_test shape: {y_test.shape}")
X_train shape: (351, 5, 1) y_train shape: (351, 1) X_test shape: (35, 5, 1) y_test shape: (35, 1)
3.1. Transformer base ¶
Para la construcción del modelo, se crean dos funciones auxiliares, una que crea el encoder y otra que construye el modelo.
El modelo se construye utilizando la API funcional de Keras, donde se define un encoder que utiliza capas de atención y capas densas para procesar las secuencias de datos. El bloque transformer_encoder cuenta con:
- Capas de normalización de datos (
LayerNormalization). - Capas de atención multi-cabeza (
MultiHeadAttention). - Sub-red feed-forward con capas convolucionales (
Conv1D). - Capa de dropout para regularización (
Dropout). - Conexiones residuales para mejorar el flujo de gradientes.
🔮 MultiHeadAttention: Esta capa es fundamental en las arquitecturas de Transformers y se utiliza para permitir que el modelo "atienda" o preste atención a diferentes partes de una secuencia de entrada. En lugar de una sola cabeza de atención, utiliza múltiples "cabezas" de atención que aprenden a enfocar en diferentes aspectos de la información de entrada de manera paralela (en este caso, la serie temporal). Luego, las salidas de estas múltiples cabezas se concatenan y se proyectan linealmente. Esto permite que el modelo capture relaciones complejas y de largo alcance dentro de la secuencia, mejorando su capacidad para comprender el contexto.
- Usos comunes: En el PNL, es fundamental en modelos de transformadores para traducción, resumen, clasificación de texto, respuesta a preguntas y generación de texto, al entender relaciones entre palabras distantes. También se aplica en visión por computadora para tareas como clasificación de imágenes, detección de objetos y segmentación, al capturar dependencias espaciales en imágenes.
- Dimensiones de entrada/salida: Usualmente, la capa toma tres entradas: query, key y value. Cada una con forma
(batch_size, sequence_length, embedding_dim). En la auto-atención, estas tres entradas suelen ser el mismo tensor o transformaciones lineales del mismo. Como salida, se tiene la misma forma que la query de entrada:(batch_size, query_sequence_length, embedding_dim)
🔮 Conv1D: La capa Conv1D realiza una operación de convolución unidimensional. Esto significa que aplica un filtro convolucional deslizante a lo largo de una única dimensión de la entrada. Cada filtro se "desliza" sobre la secuencia de entrada, calculando el producto escalar entre los valores del filtro y los valores correspondientes en la entrada.
- Usos comunes: Es muy utilizada en PNL para extraer características de secuencias de texto (por ejemplo, reconocer n-gramas o patrones de palabras). También se aplica en el procesamiento de señales de tiempo (como audio o datos de sensores) para detectar patrones temporales.
- Dimensiones de entrada/salida: Típicamente, toma una entrada con forma
(batch_size, steps, features)y produce una salida con forma similar, pero con las steps reducidas según el tamaño del filtro y el stride, y el número de features determinado por la cantidad de filtros.
🔮 GlobalAveragePooling1D: Esta capa es una forma de reducción de dimensionalidad. Para cada característica en la entrada, calcula el promedio de todos los valores a lo largo de la dimensión de la secuencia. En otras palabras, toma una secuencia de vectores y la convierte en un solo vector promediando cada característica de forma independiente a lo largo del eje temporal.
- Usos comunes: A menudo se utiliza después de una capa convolucional (como Conv1D) para aplanar las características extraídas y prepararlas para una capa densa (totalmente conectada). Sirve como una forma de resumir la información más importante de la secuencia sin perder demasiada información clave, y ayuda a reducir el número de parámetros del modelo, lo que puede prevenir el sobreajuste.
- Dimensiones de entrada/salida: Si la entrada es
(batch_size, steps, features), la salida será(batch_size, features), dondefeatureses el número de canales de características.
Función transformer_encoder para crear el encoder:
def transformer_block(
inputs: keras.layers, head_size: int, num_heads: int, ff_dim: int, dropout: float = 0
) -> keras.layers:
"""
TODO: Mejorar esta función cambiando a una Layer de Keras personalizada + Test de verificación
Construye un bloque codificador tipo Transformer.
Args:
inputs (keras.Layer): Capa de entrada al encoder.
head_size (int): Dimensión de cada cabeza de atención.
num_heads (int): Número de cabezas de atención.
ff_dim (int): Dimensión de la red feed-forward interna.
dropout (float, optional): Tasa de dropout. Por defecto 0.
Returns:
keras.Layer: Capa de salida del bloque codificador Transformer.
"""
# Normalización de la entrada
x = LayerNormalization(epsilon=1e-6)(inputs) # Shape: (batch_size, seq_len, features) -> (batch_size, seq_len, features)
# Atención multi-cabeza
x = MultiHeadAttention(key_dim=head_size, num_heads=num_heads, dropout=dropout)(x, x) # Shape: (batch_size, seq_len, features) -> (batch_size, seq_len, features)
# Conexión residual
x = Dropout(dropout)(x) # Shape: (batch_size, seq_len, features) -> (batch_size, seq_len, features)
res = x + inputs # Shape: (batch_size, seq_len, features) -> (batch_size, seq_len, features)
# Normalización de la salida
x = LayerNormalization(epsilon=1e-6)(res) # Shape: (batch_size, seq_len, features) -> (batch_size, seq_len, features)
# Capa densa feed-forward
x = Conv1D(filters=ff_dim, kernel_size=1, activation="relu")(x) # Shape: (batch_size, seq_len, features) -> (batch_size, seq_len, ff_dim)
x = Dropout(dropout)(x) # Shape: (batch_size, seq_len, ff_dim) -> (batch_size, seq_len, ff_dim)
return Conv1D(filters=inputs.shape[-1], kernel_size=1)(x) + res # Shape: (batch_size, seq_len, ff_dim) -> (batch_size, seq_len, features)
Función build_model para construir el modelo:
def build_model(
input_shape: tuple,
head_size: int,
num_heads: int,
ff_dim: int,
num_transformer_blocks: int,
mlp_units: list,
dropout: float = 0,
mlp_dropout: float = 0,
) -> keras.Model:
"""
Construye un modelo basado en bloques Transformer para series temporales.
Args:
input_shape (tuple): Forma de la entrada (longitud de la secuencia, número de características).
head_size (int): Dimensión de cada cabeza de atención.
num_heads (int): Número de cabezas de atención en MultiHeadAttention.
ff_dim (int): Dimensión de la red feed-forward interna de cada bloque Transformer.
num_transformer_blocks (int): Número de bloques codificadores Transformer a apilar.
mlp_units (list): Lista con el número de unidades para cada capa densa (MLP) posterior.
dropout (float, opcional): Tasa de dropout en los bloques Transformer. Por defecto 0.
mlp_dropout (float, opcional): Tasa de dropout en las capas MLP. Por defecto 0.
Returns:
keras.Model: Modelo Keras listo para compilar y entrenar.
"""
inputs = Input(shape=input_shape) # Shape: (seq_len, num_features)
x = inputs
# Apilar bloques de codificador Transformer
for _ in range(num_transformer_blocks):
x = transformer_block(x, head_size, num_heads, ff_dim, dropout) # Shape: (batch_size, seq_len, num_features) -> (batch_size, seq_len, num_features)
# Promediar la salida a lo largo de la secuencia
x = GlobalAveragePooling1D(data_format="channels_first")(x) # Shape: (batch_size, seq_len, num_features) -> (batch_size, num_features)
# Capa MLP
for dim in mlp_units:
x = Dense(dim, activation="relu")(x) # Shape: (batch_size, num_features) -> (batch_size, dim)
x = Dropout(mlp_dropout)(x) # Shape: (batch_size, dim) -> (batch_size, dim)
outputs = Dense(1)(x) # Shape: (batch_size, dim) -> (batch_size, 1)
return Model(inputs, outputs)
Una vez definidas las funciones auxiliares, se procede a crear el modelo:
input_shape = X_train.shape[1:]
model = build_model(
input_shape,
head_size=256,
num_heads=4,
ff_dim=4,
num_transformer_blocks=4,
mlp_units=[128],
dropout=0.25,
mlp_dropout=0.4
)
Mostramos un resumen del modelo:
model.summary()
Model: "functional"
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ Connected to ┃ ┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩ │ input_layer │ (None, 5, 1) │ 0 │ - │ │ (InputLayer) │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ layer_normalization │ (None, 5, 1) │ 2 │ input_layer[0][0] │ │ (LayerNormalizatio… │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ multi_head_attenti… │ (None, 5, 1) │ 7,169 │ layer_normalizat… │ │ (MultiHeadAttentio… │ │ │ layer_normalizat… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dropout_1 (Dropout) │ (None, 5, 1) │ 0 │ multi_head_atten… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ add (Add) │ (None, 5, 1) │ 0 │ dropout_1[0][0], │ │ │ │ │ input_layer[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ layer_normalizatio… │ (None, 5, 1) │ 2 │ add[0][0] │ │ (LayerNormalizatio… │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ conv1d (Conv1D) │ (None, 5, 4) │ 8 │ layer_normalizat… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dropout_2 (Dropout) │ (None, 5, 4) │ 0 │ conv1d[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ conv1d_1 (Conv1D) │ (None, 5, 1) │ 5 │ dropout_2[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ add_1 (Add) │ (None, 5, 1) │ 0 │ conv1d_1[0][0], │ │ │ │ │ add[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ layer_normalizatio… │ (None, 5, 1) │ 2 │ add_1[0][0] │ │ (LayerNormalizatio… │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ multi_head_attenti… │ (None, 5, 1) │ 7,169 │ layer_normalizat… │ │ (MultiHeadAttentio… │ │ │ layer_normalizat… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dropout_4 (Dropout) │ (None, 5, 1) │ 0 │ multi_head_atten… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ add_2 (Add) │ (None, 5, 1) │ 0 │ dropout_4[0][0], │ │ │ │ │ add_1[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ layer_normalizatio… │ (None, 5, 1) │ 2 │ add_2[0][0] │ │ (LayerNormalizatio… │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ conv1d_2 (Conv1D) │ (None, 5, 4) │ 8 │ layer_normalizat… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dropout_5 (Dropout) │ (None, 5, 4) │ 0 │ conv1d_2[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ conv1d_3 (Conv1D) │ (None, 5, 1) │ 5 │ dropout_5[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ add_3 (Add) │ (None, 5, 1) │ 0 │ conv1d_3[0][0], │ │ │ │ │ add_2[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ layer_normalizatio… │ (None, 5, 1) │ 2 │ add_3[0][0] │ │ (LayerNormalizatio… │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ multi_head_attenti… │ (None, 5, 1) │ 7,169 │ layer_normalizat… │ │ (MultiHeadAttentio… │ │ │ layer_normalizat… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dropout_7 (Dropout) │ (None, 5, 1) │ 0 │ multi_head_atten… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ add_4 (Add) │ (None, 5, 1) │ 0 │ dropout_7[0][0], │ │ │ │ │ add_3[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ layer_normalizatio… │ (None, 5, 1) │ 2 │ add_4[0][0] │ │ (LayerNormalizatio… │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ conv1d_4 (Conv1D) │ (None, 5, 4) │ 8 │ layer_normalizat… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dropout_8 (Dropout) │ (None, 5, 4) │ 0 │ conv1d_4[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ conv1d_5 (Conv1D) │ (None, 5, 1) │ 5 │ dropout_8[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ add_5 (Add) │ (None, 5, 1) │ 0 │ conv1d_5[0][0], │ │ │ │ │ add_4[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ layer_normalizatio… │ (None, 5, 1) │ 2 │ add_5[0][0] │ │ (LayerNormalizatio… │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ multi_head_attenti… │ (None, 5, 1) │ 7,169 │ layer_normalizat… │ │ (MultiHeadAttentio… │ │ │ layer_normalizat… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dropout_10 │ (None, 5, 1) │ 0 │ multi_head_atten… │ │ (Dropout) │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ add_6 (Add) │ (None, 5, 1) │ 0 │ dropout_10[0][0], │ │ │ │ │ add_5[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ layer_normalizatio… │ (None, 5, 1) │ 2 │ add_6[0][0] │ │ (LayerNormalizatio… │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ conv1d_6 (Conv1D) │ (None, 5, 4) │ 8 │ layer_normalizat… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dropout_11 │ (None, 5, 4) │ 0 │ conv1d_6[0][0] │ │ (Dropout) │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ conv1d_7 (Conv1D) │ (None, 5, 1) │ 5 │ dropout_11[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ add_7 (Add) │ (None, 5, 1) │ 0 │ conv1d_7[0][0], │ │ │ │ │ add_6[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ global_average_poo… │ (None, 5) │ 0 │ add_7[0][0] │ │ (GlobalAveragePool… │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dense (Dense) │ (None, 128) │ 768 │ global_average_p… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dropout_12 │ (None, 128) │ 0 │ dense[0][0] │ │ (Dropout) │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dense_1 (Dense) │ (None, 1) │ 129 │ dropout_12[0][0] │ └─────────────────────┴───────────────────┴────────────┴───────────────────┘
Total params: 29,641 (115.79 KB)
Trainable params: 29,641 (115.79 KB)
Non-trainable params: 0 (0.00 B)
Finalmente, compilamos el modelo y lo entrenamos:
model.compile(
loss=MeanSquaredError(), optimizer=Adam(learning_rate=1e-4), metrics=[MeanAbsoluteError()]
)
history = model.fit(X_train, y_train, validation_split=0.2, epochs=500, batch_size=64, callbacks=[EarlyStopping(patience=10, restore_best_weights=True)])
Epoch 1/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 6s 123ms/step - loss: 2931908352.0000 - mean_absolute_error: 51398.0898 - val_loss: 5286720512.0000 - val_mean_absolute_error: 72658.4375 Epoch 2/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 2639362816.0000 - mean_absolute_error: 48537.7188 - val_loss: 4933776896.0000 - val_mean_absolute_error: 70189.2500 Epoch 3/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 30ms/step - loss: 2355904256.0000 - mean_absolute_error: 45970.3320 - val_loss: 4596278784.0000 - val_mean_absolute_error: 67744.0625 Epoch 4/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 30ms/step - loss: 2402662400.0000 - mean_absolute_error: 46137.6758 - val_loss: 4269745664.0000 - val_mean_absolute_error: 65291.1602 Epoch 5/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 30ms/step - loss: 2094848384.0000 - mean_absolute_error: 42966.1719 - val_loss: 3960026624.0000 - val_mean_absolute_error: 62876.1328 Epoch 6/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 30ms/step - loss: 2153898752.0000 - mean_absolute_error: 43478.9375 - val_loss: 3663840000.0000 - val_mean_absolute_error: 60476.4141 Epoch 7/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 1822855040.0000 - mean_absolute_error: 39611.8516 - val_loss: 3384778752.0000 - val_mean_absolute_error: 58124.8398 Epoch 8/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 30ms/step - loss: 1766975488.0000 - mean_absolute_error: 38791.3711 - val_loss: 3121116416.0000 - val_mean_absolute_error: 55812.0273 Epoch 9/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 30ms/step - loss: 1664151168.0000 - mean_absolute_error: 37863.9180 - val_loss: 2871860736.0000 - val_mean_absolute_error: 53533.7539 Epoch 10/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 30ms/step - loss: 1516417664.0000 - mean_absolute_error: 35468.4648 - val_loss: 2638359296.0000 - val_mean_absolute_error: 51307.7305 Epoch 11/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 1429500800.0000 - mean_absolute_error: 34253.8555 - val_loss: 2419390720.0000 - val_mean_absolute_error: 49128.6641 Epoch 12/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 1288841344.0000 - mean_absolute_error: 32469.5078 - val_loss: 2215454976.0000 - val_mean_absolute_error: 47008.3711 Epoch 13/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 1326064512.0000 - mean_absolute_error: 32841.6914 - val_loss: 2025183744.0000 - val_mean_absolute_error: 44939.9219 Epoch 14/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 1224336512.0000 - mean_absolute_error: 31521.4570 - val_loss: 1847073408.0000 - val_mean_absolute_error: 42913.3828 Epoch 15/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 982773632.0000 - mean_absolute_error: 27354.8379 - val_loss: 1683441664.0000 - val_mean_absolute_error: 40963.2617 Epoch 16/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 1000688512.0000 - mean_absolute_error: 28006.0879 - val_loss: 1532811904.0000 - val_mean_absolute_error: 39082.1406 Epoch 17/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 30ms/step - loss: 937902400.0000 - mean_absolute_error: 26942.1680 - val_loss: 1392712704.0000 - val_mean_absolute_error: 37247.3242 Epoch 18/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 837962944.0000 - mean_absolute_error: 24932.8770 - val_loss: 1261865216.0000 - val_mean_absolute_error: 35447.9609 Epoch 19/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 893811072.0000 - mean_absolute_error: 25602.9941 - val_loss: 1140916480.0000 - val_mean_absolute_error: 33699.3086 Epoch 20/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 35ms/step - loss: 680641536.0000 - mean_absolute_error: 22225.8301 - val_loss: 1030937344.0000 - val_mean_absolute_error: 32026.4336 Epoch 21/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 669878912.0000 - mean_absolute_error: 21571.4512 - val_loss: 931465920.0000 - val_mean_absolute_error: 30434.2754 Epoch 22/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 698858176.0000 - mean_absolute_error: 22865.0820 - val_loss: 839636928.0000 - val_mean_absolute_error: 28886.5996 Epoch 23/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 622867648.0000 - mean_absolute_error: 20669.9629 - val_loss: 754100352.0000 - val_mean_absolute_error: 27366.2988 Epoch 24/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 624596224.0000 - mean_absolute_error: 21123.2812 - val_loss: 676065600.0000 - val_mean_absolute_error: 25901.4688 Epoch 25/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 590029120.0000 - mean_absolute_error: 19945.7012 - val_loss: 605426624.0000 - val_mean_absolute_error: 24499.9375 Epoch 26/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 35ms/step - loss: 564406400.0000 - mean_absolute_error: 19614.1465 - val_loss: 539815424.0000 - val_mean_absolute_error: 23122.1191 Epoch 27/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 453636192.0000 - mean_absolute_error: 17874.6094 - val_loss: 480884416.0000 - val_mean_absolute_error: 21810.4434 Epoch 28/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 530919584.0000 - mean_absolute_error: 18751.7715 - val_loss: 426556608.0000 - val_mean_absolute_error: 20527.0293 Epoch 29/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 491364704.0000 - mean_absolute_error: 18230.4277 - val_loss: 378275712.0000 - val_mean_absolute_error: 19314.9785 Epoch 30/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 491620352.0000 - mean_absolute_error: 16982.0430 - val_loss: 333226176.0000 - val_mean_absolute_error: 18110.9297 Epoch 31/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 482641760.0000 - mean_absolute_error: 17460.0938 - val_loss: 292651552.0000 - val_mean_absolute_error: 16953.3379 Epoch 32/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 399528000.0000 - mean_absolute_error: 15983.8359 - val_loss: 256699616.0000 - val_mean_absolute_error: 15857.0596 Epoch 33/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 376088416.0000 - mean_absolute_error: 15040.8877 - val_loss: 225938448.0000 - val_mean_absolute_error: 14854.8682 Epoch 34/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 380211040.0000 - mean_absolute_error: 15420.9658 - val_loss: 199179344.0000 - val_mean_absolute_error: 13924.4365 Epoch 35/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 356347648.0000 - mean_absolute_error: 14851.3203 - val_loss: 175053776.0000 - val_mean_absolute_error: 13028.6104 Epoch 36/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 314272128.0000 - mean_absolute_error: 14491.9766 - val_loss: 153875216.0000 - val_mean_absolute_error: 12187.9434 Epoch 37/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 358015744.0000 - mean_absolute_error: 15072.0410 - val_loss: 134931776.0000 - val_mean_absolute_error: 11383.3975 Epoch 38/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 321821824.0000 - mean_absolute_error: 14341.9365 - val_loss: 117890808.0000 - val_mean_absolute_error: 10607.5283 Epoch 39/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 313459296.0000 - mean_absolute_error: 14144.0908 - val_loss: 103322952.0000 - val_mean_absolute_error: 9896.0127 Epoch 40/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 266034704.0000 - mean_absolute_error: 12963.7500 - val_loss: 91977600.0000 - val_mean_absolute_error: 9304.1670 Epoch 41/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 304792576.0000 - mean_absolute_error: 13844.5557 - val_loss: 82140344.0000 - val_mean_absolute_error: 8758.5811 Epoch 42/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 308871968.0000 - mean_absolute_error: 14257.7002 - val_loss: 71903720.0000 - val_mean_absolute_error: 8152.0405 Epoch 43/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 330430528.0000 - mean_absolute_error: 14321.3408 - val_loss: 62668980.0000 - val_mean_absolute_error: 7563.1084 Epoch 44/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 311064000.0000 - mean_absolute_error: 14163.4746 - val_loss: 54519156.0000 - val_mean_absolute_error: 7002.1860 Epoch 45/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 283901792.0000 - mean_absolute_error: 13525.2988 - val_loss: 48577848.0000 - val_mean_absolute_error: 6563.0220 Epoch 46/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 293986208.0000 - mean_absolute_error: 13480.3008 - val_loss: 43261740.0000 - val_mean_absolute_error: 6147.5107 Epoch 47/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 274570752.0000 - mean_absolute_error: 13154.0439 - val_loss: 39947856.0000 - val_mean_absolute_error: 5878.8496 Epoch 48/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 297522656.0000 - mean_absolute_error: 13536.9902 - val_loss: 36427064.0000 - val_mean_absolute_error: 5578.7085 Epoch 49/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 307785888.0000 - mean_absolute_error: 14076.9814 - val_loss: 32613002.0000 - val_mean_absolute_error: 5233.3696 Epoch 50/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 269674656.0000 - mean_absolute_error: 13498.2168 - val_loss: 29218350.0000 - val_mean_absolute_error: 4904.6953 Epoch 51/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 267554224.0000 - mean_absolute_error: 12802.0449 - val_loss: 26793076.0000 - val_mean_absolute_error: 4661.5991 Epoch 52/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 246900912.0000 - mean_absolute_error: 12444.8506 - val_loss: 24978186.0000 - val_mean_absolute_error: 4476.9609 Epoch 53/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 281850784.0000 - mean_absolute_error: 13342.5068 - val_loss: 23457820.0000 - val_mean_absolute_error: 4318.7759 Epoch 54/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 255747712.0000 - mean_absolute_error: 12770.3018 - val_loss: 22254964.0000 - val_mean_absolute_error: 4193.4150 Epoch 55/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 315603168.0000 - mean_absolute_error: 13749.6230 - val_loss: 20899612.0000 - val_mean_absolute_error: 4047.4819 Epoch 56/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 274520704.0000 - mean_absolute_error: 13710.1826 - val_loss: 19971250.0000 - val_mean_absolute_error: 3943.6821 Epoch 57/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 299993792.0000 - mean_absolute_error: 13600.0527 - val_loss: 18726492.0000 - val_mean_absolute_error: 3799.0325 Epoch 58/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 257014640.0000 - mean_absolute_error: 12386.6719 - val_loss: 18148048.0000 - val_mean_absolute_error: 3729.4006 Epoch 59/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 311186656.0000 - mean_absolute_error: 14452.3936 - val_loss: 17602358.0000 - val_mean_absolute_error: 3662.2100 Epoch 60/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 296167296.0000 - mean_absolute_error: 13961.6172 - val_loss: 16640874.0000 - val_mean_absolute_error: 3539.9583 Epoch 61/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 271627840.0000 - mean_absolute_error: 12718.7832 - val_loss: 15458378.0000 - val_mean_absolute_error: 3381.9343 Epoch 62/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 254627664.0000 - mean_absolute_error: 12738.7910 - val_loss: 14275945.0000 - val_mean_absolute_error: 3218.8328 Epoch 63/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 256961296.0000 - mean_absolute_error: 12568.0752 - val_loss: 12969874.0000 - val_mean_absolute_error: 3034.0991 Epoch 64/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 288503744.0000 - mean_absolute_error: 13417.4053 - val_loss: 12341471.0000 - val_mean_absolute_error: 2942.4541 Epoch 65/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 303530592.0000 - mean_absolute_error: 13700.5850 - val_loss: 11820032.0000 - val_mean_absolute_error: 2867.5664 Epoch 66/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 277053088.0000 - mean_absolute_error: 13013.0361 - val_loss: 11121140.0000 - val_mean_absolute_error: 2764.3877 Epoch 67/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 302792512.0000 - mean_absolute_error: 14001.8984 - val_loss: 10781410.0000 - val_mean_absolute_error: 2713.0828 Epoch 68/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 251902896.0000 - mean_absolute_error: 12564.7031 - val_loss: 10635845.0000 - val_mean_absolute_error: 2690.5264 Epoch 69/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 282023040.0000 - mean_absolute_error: 13452.2646 - val_loss: 10476130.0000 - val_mean_absolute_error: 2665.3640 Epoch 70/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 288038912.0000 - mean_absolute_error: 13639.7227 - val_loss: 10543733.0000 - val_mean_absolute_error: 2675.8613 Epoch 71/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 279772672.0000 - mean_absolute_error: 13359.6836 - val_loss: 10550904.0000 - val_mean_absolute_error: 2676.8542 Epoch 72/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 238857616.0000 - mean_absolute_error: 12379.2637 - val_loss: 10519511.0000 - val_mean_absolute_error: 2671.8113 Epoch 73/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 252862224.0000 - mean_absolute_error: 12602.3203 - val_loss: 10090749.0000 - val_mean_absolute_error: 2602.6462 Epoch 74/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 281704416.0000 - mean_absolute_error: 13301.4229 - val_loss: 9922549.0000 - val_mean_absolute_error: 2574.4917 Epoch 75/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 256188384.0000 - mean_absolute_error: 12501.0049 - val_loss: 9616195.0000 - val_mean_absolute_error: 2523.1748 Epoch 76/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 281778304.0000 - mean_absolute_error: 13249.4209 - val_loss: 9574454.0000 - val_mean_absolute_error: 2516.0027 Epoch 77/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 281540832.0000 - mean_absolute_error: 13515.3721 - val_loss: 9311632.0000 - val_mean_absolute_error: 2471.9656 Epoch 78/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 274452480.0000 - mean_absolute_error: 13464.8154 - val_loss: 8899014.0000 - val_mean_absolute_error: 2401.8447 Epoch 79/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 248801536.0000 - mean_absolute_error: 12595.7344 - val_loss: 8435545.0000 - val_mean_absolute_error: 2324.0674 Epoch 80/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 251568944.0000 - mean_absolute_error: 12798.9775 - val_loss: 8216730.0000 - val_mean_absolute_error: 2292.0630 Epoch 81/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 265422704.0000 - mean_absolute_error: 13159.1533 - val_loss: 8034055.0000 - val_mean_absolute_error: 2264.6853 Epoch 82/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 263162624.0000 - mean_absolute_error: 12902.0371 - val_loss: 7923388.5000 - val_mean_absolute_error: 2247.5256 Epoch 83/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 286395584.0000 - mean_absolute_error: 13533.2969 - val_loss: 7985430.5000 - val_mean_absolute_error: 2257.0803 Epoch 84/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 283268256.0000 - mean_absolute_error: 12931.9658 - val_loss: 8416421.0000 - val_mean_absolute_error: 2320.8193 Epoch 85/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 255768512.0000 - mean_absolute_error: 12507.1299 - val_loss: 8813763.0000 - val_mean_absolute_error: 2386.1355 Epoch 86/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 210062496.0000 - mean_absolute_error: 11652.9121 - val_loss: 9051601.0000 - val_mean_absolute_error: 2426.3877 Epoch 87/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 279747360.0000 - mean_absolute_error: 13140.8652 - val_loss: 9344012.0000 - val_mean_absolute_error: 2475.8311 Epoch 88/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 277476032.0000 - mean_absolute_error: 12948.1104 - val_loss: 9669172.0000 - val_mean_absolute_error: 2529.9404 Epoch 89/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 258612144.0000 - mean_absolute_error: 12810.0566 - val_loss: 10119032.0000 - val_mean_absolute_error: 2604.7900 Epoch 90/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 265059008.0000 - mean_absolute_error: 12538.5889 - val_loss: 10129440.0000 - val_mean_absolute_error: 2606.3706 Epoch 91/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 246241808.0000 - mean_absolute_error: 12450.1191 - val_loss: 10094276.0000 - val_mean_absolute_error: 2600.4292 Epoch 92/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 31ms/step - loss: 260589920.0000 - mean_absolute_error: 12751.0879 - val_loss: 10066775.0000 - val_mean_absolute_error: 2595.7732
Una vez entrenado el modelo, se procede a verificar los resultados. Para esto, se utilizan varias funciones auxiliares para graficar y evaluar el modelo:
def smape(y_true: np.ndarray, y_pred: np.ndarray) -> float:
"""
Calcula el Symmetric Mean Absolute Percentage Error (sMAPE) entre los valores reales y predichos.
Args:
y_true (np.ndarray): Valores reales.
y_pred (np.ndarray): Valores predichos.
Returns:
float: El valor de sMAPE expresado en porcentaje.
"""
return 100 / len(y_true) * np.sum(2 * np.abs(y_pred - y_true) / (np.abs(y_pred) + np.abs(y_true)))
def quantile_loss(q: float, y: np.ndarray, f: np.ndarray) -> np.ndarray:
"""
Calcula la pérdida de cuantiles (quantile loss) para una predicción dada.
Args:
q (float): Cuantil deseado (por ejemplo, 0.5 para la mediana).
y (np.ndarray): Valores reales.
f (np.ndarray): Valores predichos.
Returns:
np.ndarray: Pérdida de cuantiles para cada elemento.
"""
e = y - f
return np.maximum(q * e, (q - 1) * e)
def calculate_metrics(y_true: np.ndarray, y_pred: np.ndarray) -> dict:
"""
Calcula varias métricas de evaluación para comparar valores reales y predichos.
Args:
y_true (np.ndarray): Valores reales.
y_pred (np.ndarray): Valores predichos.
Returns:
dict: Diccionario con las métricas RMSE, MAPE, MAE, MSE, sMAPE, RRMSE y Quantile Loss.
"""
return {
"RMSE": np.sqrt(metrics.mean_squared_error(y_true, y_pred)),
"MAPE": metrics.mean_absolute_percentage_error(y_true, y_pred) * 100,
"MAE": metrics.mean_absolute_error(y_true, y_pred),
"MSE": metrics.mean_squared_error(y_true, y_pred),
"sMAPE": smape(y_true, y_pred),
"RRMSE": np.sqrt(metrics.mean_squared_error(y_true, y_pred)) / np.mean(y_true) * 100,
"Quantile Loss": np.mean(quantile_loss(0.5, y_true, y_pred)),
}
def plot_actual_vs_predicted(y_true: np.ndarray, y_pred: np.ndarray, filename: str, fig_size=(8, 8)) -> None:
"""
Grafica los valores reales frente a los valores predichos y guarda la figura en un archivo.
Args:
y_true (np.ndarray): Valores reales.
y_pred (np.ndarray): Valores predichos.
filename (str): Nombre del archivo donde se guardará la figura.
Returns:
None
"""
plt.figure(figsize=fig_size, dpi=600)
plt.plot(y_true, label="Actual", color="blue", linewidth=2)
plt.plot(y_pred, label="Predicted", color="red", linewidth=2, linestyle="--")
plt.legend(fontsize="medium", loc="upper left")
plt.xlabel("Time (Day)", fontsize=16, fontweight="bold")
plt.ylabel("BTC (BTC/UST)", fontsize=16, fontweight="bold")
plt.xticks(fontsize=14, fontweight="bold")
plt.yticks(fontsize=14, fontweight="bold")
plt.title("Actual vs Predicted", fontsize=16, fontweight="bold")
plt.savefig(filename, format="jpeg", dpi=600)
plt.tight_layout()
plt.show()
def plot_loss(history: History, filename: str, fig_size=(8, 8)) -> None:
"""
Grafica la pérdida de entrenamiento y validación a lo largo de las épocas y guarda la figura en un archivo.
Args:
history (keras.callbacks.History): Objeto History devuelto por el método fit() de Keras.
filename (str): Nombre del archivo donde se guardará la figura.
Returns:
None
"""
plt.figure(figsize=fig_size, dpi=600)
plt.plot(history.history["loss"], label="Training Loss", color="g", linewidth=2)
plt.plot(history.history["val_loss"], label="Validation Loss", color="b", linewidth=2)
plt.legend(fontsize="medium", loc="upper right")
plt.xlabel("Epochs", fontsize=16, fontweight="bold")
plt.ylabel("Loss", fontsize=16, fontweight="bold")
plt.xticks(fontsize=14, fontweight="bold")
plt.yticks(fontsize=14, fontweight="bold")
plt.title("Training and Validation Loss", fontsize=16, fontweight="bold")
plt.savefig(filename, format="jpeg", dpi=600)
plt.tight_layout()
plt.show()
Mostramos el gráfico de entrenamiento:
plot_loss(history, "resources/base_model_loss.jpeg", fig_size=(8, 8))
Evaluamos el modelo en el conjunto de test:
test_loss = model.evaluate(X_test, y_test, verbose=1, return_dict=True)
pprint(test_loss)
2/2 ━━━━━━━━━━━━━━━━━━━━ 0s 20ms/step - loss: 11054148.0000 - mean_absolute_error: 2437.5195 {'loss': 11054148.0, 'mean_absolute_error': 2437.51953125}
Calculamos las métricas de evaluación:
pred_test = model.predict(X_test)
metrics_dict_test = calculate_metrics(y_test, pred_test)
print("\nTest Set Metrics:")
for metric_name, metric_value in metrics_dict_test.items():
print(f"Score ({metric_name}): {metric_value}")
2/2 ━━━━━━━━━━━━━━━━━━━━ 0s 245ms/step Test Set Metrics: Score (RMSE): 3324.778150848964 Score (MAPE): 2.8379590928861047 Score (MAE): 2437.519883928571 Score (MSE): 11054149.752362655 Score (sMAPE): 2.8624316120760143 Score (RRMSE): 3.904685378086401 Score (Quantile Loss): 1218.7599419642854
Guardamos las métricas de evaluación:
df_line = pd.DataFrame({
"model": "CustTransf",
"mae": metrics_dict_test["MAE"],
"mape": metrics_dict_test["MAPE"],
"rmse": metrics_dict_test["RMSE"]
}, index=[0])
df_metrics = pd.concat([df_metrics, df_line], ignore_index=True)
df_metrics
| model | mae | mape | rmse | |
|---|---|---|---|---|
| 0 | Naive | 1735.726500 | 1.888647 | 2434.504211 |
| 1 | ARIMA(1,1,35) | 1891.662281 | 2.063588 | 2546.257391 |
| 2 | ARIMA(0,1,0) | 1735.726500 | 1.894494 | 2434.504211 |
| 3 | Prophet | 2582.551322 | 2.871448 | 3621.703076 |
| 4 | XGBoost | 4319.031819 | 4.609317 | 5555.304516 |
| 5 | LSTM | 2432.965561 | 2.645095 | 3192.781096 |
| 6 | CustTransf | 2437.519884 | 2.837959 | 3324.778151 |
Finalmente, graficamos para observar los resultados de la predicción:
plot_actual_vs_predicted(y_test, pred_test, "resources/base_model_predict.jpeg")
3.2. Transformer + positional encoding ¶
El objetivo de esta sección es explorar la incorporación de un positional encoding alternativo al modelo Transformer previamente construido. Este enfoque busca mejorar la capacidad del modelo para capturar patrones temporales en los datos de series de tiempo.
Para ello, se define una nueva clase PositionalEmbedding que hereda de Layer de Keras. Esta clase implementa un positional encoding basado en la codificación de las posiciones de los datos en la secuencia. La idea es que entrada del modelo tenga una representación única que permita que éste aprenda patrones temporales de manera más efectiva.
Ejemplo básico de implementación de un Layer de Keras:
class Linear(keras.layers.Layer):
def __init__(self, units=32):
super().__init__()
self.units = units
def build(self, input_shape):
self.w = self.add_weight(
shape=(input_shape[-1], self.units),
initializer="random_normal",
trainable=True,
)
self.b = self.add_weight(
shape=(self.units,), initializer="random_normal", trainable=True
)
def call(self, inputs):
return ops.matmul(inputs, self.w) + self.b
def get_config(self):
return {"units": self.units}
💡 Idea: A diferencia de NLP, en donde los tokens son discretos y para estos casos se utiliza la capa
Embeddingde Keras, en series de tiempo, los datos son continuos, por lo que para incrustarlos en un espacio de mayor dimensión, probaremos utilizar simplemente una capaDense, en donde el objetivo es que esta capa aprenda a mapear los valores de la serie de tiempo a un espacio dimensional específico (output_dim).
Implementación de la clase PositionalEmbedding:
class PositionalEmbedding(Layer):
"""
Capa de embedding posicional para series de tiempo.
Args:
output_dim (int): Dimensión de salida de los embeddings.
seq_length (int): Longitud de la secuencia de entrada.
**kwargs: Argumentos adicionales para la clase base Layer.
"""
def __init__(self, output_dim: int, seq_length: int, **kwargs):
super().__init__(**kwargs)
self.output_dim = output_dim
self.seq_length = seq_length
def build(self) -> None:
self.token_embeddings = Dense(self.output_dim, activation=None)
self.position_embeddings = Embedding(input_dim=self.seq_length, output_dim=self.output_dim)
def call(self, inputs):
positions = np.arange(self.seq_length) # Shape: (batch_size, seq_length) -> (batch_size, seq_length)
embedded_positions = self.position_embeddings(positions) # Shape: (batch_size, seq_length) -> (batch_size, seq_length, output_dim)
embedded_tokens = self.token_embeddings(inputs) # Shape: (batch_size, seq_length) -> (batch_size, seq_length, output_dim)
# Sumar los embeddings posicionales y los embeddings de los tokens
return embedded_tokens + embedded_positions # Shape: (batch_size, seq_length, output_dim) -> (batch_size, seq_length, output_dim)
def get_config(self) -> dict:
config = super().get_config()
config.update(
{
"input_shape": self.input_shape,
"output_dim": self.output_dim,
}
)
return config
Realizamos un test de verificación:
inp = Input(shape=(WINDOW_SIZE, 1))
out = PositionalEmbedding(16)(inp)
test_model = Model(inputs=inp, outputs=out)
output = test_model.predict(keras.random.normal(shape=(WINDOW_SIZE, 1)))
x = keras.random.normal(shape=(WINDOW_SIZE, 1))
y = PositionalEmbedding(output_dim=8, seq_length=WINDOW_SIZE)(x)
y.shape
TensorShape([5, 8])
Modificamos la función build_model para incorporar la nueva capa PositionalEmbedding, creando una nueva función auxiliar build_model_with_positional_embedding:
def build_model_with_embeddings(
input_shape,
head_size,
num_heads,
ff_dim,
num_transformer_blocks,
mlp_units,
seq_length,
dropout=0,
mlp_dropout=0,
embedding_dim=None,
):
"""
Construye un modelo Transformer para series de tiempo que incorpora una capa de embedding posicional.
Args:
input_shape (tuple): Forma de la entrada (longitud de la secuencia, número de características).
head_size (int): Dimensión de cada cabeza de atención en MultiHeadAttention.
num_heads (int): Número de cabezas de atención.
ff_dim (int): Dimensión de la red feed-forward interna de cada bloque Transformer.
num_transformer_blocks (int): Número de bloques codificadores Transformer a apilar.
mlp_units (list): Lista con el número de unidades para cada capa densa (MLP) posterior.
seq_length (int): Longitud de la secuencia de entrada.
dropout (float, opcional): Tasa de dropout en los bloques Transformer. Por defecto 0.
mlp_dropout (float, opcional): Tasa de dropout en las capas MLP. Por defecto 0.
embedding_dim (int, opcional): Dimensión de salida de la capa de embedding. Si no se especifica, se utiliza `head_size`.
Returns:
keras.Model: Modelo Keras listo para compilar y entrenar.
"""
inputs = Input(shape=input_shape) # Shape: (batch_size, sequence_length, num_features) -> (batch_size, sequence_length, num_features)
x = inputs
# Determinar la dimensión de embedding:
# Si embedding_dim no se especifica, usa el `head_size` para que sea consistente con la dimensión de entrada del Transformer.
if embedding_dim is None:
embedding_dim = head_size
x = PositionalEmbedding(output_dim=embedding_dim, seq_length=seq_length)(x) # Shape: (batch_size, sequence_length, num_features) -> (batch_size, sequence_length, embedding_dim)
# Apilar bloques de codificador Transformer
for _ in range(num_transformer_blocks):
x = transformer_block(x, head_size, num_heads, ff_dim, dropout) # Shape: (batch_size, sequence_length, embedding_dim) -> (batch_size, sequence_length, embedding_dim)
x = GlobalAveragePooling1D()(x) # Shape: (batch_size, sequence_length, embedding_dim) -> (batch_size, embedding_dim)
# Capa MLP
for dim in mlp_units:
x = Dense(dim, activation="relu")(x) # Shape: (batch_size, embedding_dim) -> (batch_size, dim)
x = Dropout(mlp_dropout)(x) # Shape: (batch_size, dim) -> (batch_size, dim)
outputs = Dense(1)(x) # Shape: (batch_size, dim) -> (batch_size, 1)
return Model(inputs, outputs)
Creamos el "nuevo" modelo:
input_shape = X_train.shape[1:]
model = build_model_with_embeddings(
input_shape,
head_size=256,
num_heads=4,
ff_dim=4,
num_transformer_blocks=4,
mlp_units=[128],
seq_length=WINDOW_SIZE,
dropout=0.25,
mlp_dropout=0.4,
embedding_dim=EMMBEDDING_DIM
)
WARNING:tensorflow:From e:\Documentos\Git Repositories\uba-mia-ast2\.venv\Lib\site-packages\keras\src\backend\tensorflow\core.py:232: The name tf.placeholder is deprecated. Please use tf.compat.v1.placeholder instead.
Mostramos el resumen:
model.summary()
Model: "functional_1"
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ Connected to ┃ ┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩ │ input_layer_1 │ (None, 5, 1) │ 0 │ - │ │ (InputLayer) │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ positional_embeddi… │ (None, 5, 8) │ 56 │ input_layer_1[0]… │ │ (PositionalEmbeddi… │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ layer_normalizatio… │ (None, 5, 8) │ 16 │ positional_embed… │ │ (LayerNormalizatio… │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ multi_head_attenti… │ (None, 5, 8) │ 35,848 │ layer_normalizat… │ │ (MultiHeadAttentio… │ │ │ layer_normalizat… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dropout_14 │ (None, 5, 8) │ 0 │ multi_head_atten… │ │ (Dropout) │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ add_8 (Add) │ (None, 5, 8) │ 0 │ dropout_14[0][0], │ │ │ │ │ positional_embed… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ layer_normalizatio… │ (None, 5, 8) │ 16 │ add_8[0][0] │ │ (LayerNormalizatio… │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ conv1d_8 (Conv1D) │ (None, 5, 4) │ 36 │ layer_normalizat… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dropout_15 │ (None, 5, 4) │ 0 │ conv1d_8[0][0] │ │ (Dropout) │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ conv1d_9 (Conv1D) │ (None, 5, 8) │ 40 │ dropout_15[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ add_9 (Add) │ (None, 5, 8) │ 0 │ conv1d_9[0][0], │ │ │ │ │ add_8[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ layer_normalizatio… │ (None, 5, 8) │ 16 │ add_9[0][0] │ │ (LayerNormalizatio… │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ multi_head_attenti… │ (None, 5, 8) │ 35,848 │ layer_normalizat… │ │ (MultiHeadAttentio… │ │ │ layer_normalizat… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dropout_17 │ (None, 5, 8) │ 0 │ multi_head_atten… │ │ (Dropout) │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ add_10 (Add) │ (None, 5, 8) │ 0 │ dropout_17[0][0], │ │ │ │ │ add_9[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ layer_normalizatio… │ (None, 5, 8) │ 16 │ add_10[0][0] │ │ (LayerNormalizatio… │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ conv1d_10 (Conv1D) │ (None, 5, 4) │ 36 │ layer_normalizat… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dropout_18 │ (None, 5, 4) │ 0 │ conv1d_10[0][0] │ │ (Dropout) │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ conv1d_11 (Conv1D) │ (None, 5, 8) │ 40 │ dropout_18[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ add_11 (Add) │ (None, 5, 8) │ 0 │ conv1d_11[0][0], │ │ │ │ │ add_10[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ layer_normalizatio… │ (None, 5, 8) │ 16 │ add_11[0][0] │ │ (LayerNormalizatio… │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ multi_head_attenti… │ (None, 5, 8) │ 35,848 │ layer_normalizat… │ │ (MultiHeadAttentio… │ │ │ layer_normalizat… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dropout_20 │ (None, 5, 8) │ 0 │ multi_head_atten… │ │ (Dropout) │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ add_12 (Add) │ (None, 5, 8) │ 0 │ dropout_20[0][0], │ │ │ │ │ add_11[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ layer_normalizatio… │ (None, 5, 8) │ 16 │ add_12[0][0] │ │ (LayerNormalizatio… │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ conv1d_12 (Conv1D) │ (None, 5, 4) │ 36 │ layer_normalizat… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dropout_21 │ (None, 5, 4) │ 0 │ conv1d_12[0][0] │ │ (Dropout) │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ conv1d_13 (Conv1D) │ (None, 5, 8) │ 40 │ dropout_21[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ add_13 (Add) │ (None, 5, 8) │ 0 │ conv1d_13[0][0], │ │ │ │ │ add_12[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ layer_normalizatio… │ (None, 5, 8) │ 16 │ add_13[0][0] │ │ (LayerNormalizatio… │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ multi_head_attenti… │ (None, 5, 8) │ 35,848 │ layer_normalizat… │ │ (MultiHeadAttentio… │ │ │ layer_normalizat… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dropout_23 │ (None, 5, 8) │ 0 │ multi_head_atten… │ │ (Dropout) │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ add_14 (Add) │ (None, 5, 8) │ 0 │ dropout_23[0][0], │ │ │ │ │ add_13[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ layer_normalizatio… │ (None, 5, 8) │ 16 │ add_14[0][0] │ │ (LayerNormalizatio… │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ conv1d_14 (Conv1D) │ (None, 5, 4) │ 36 │ layer_normalizat… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dropout_24 │ (None, 5, 4) │ 0 │ conv1d_14[0][0] │ │ (Dropout) │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ conv1d_15 (Conv1D) │ (None, 5, 8) │ 40 │ dropout_24[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ add_15 (Add) │ (None, 5, 8) │ 0 │ conv1d_15[0][0], │ │ │ │ │ add_14[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ global_average_poo… │ (None, 8) │ 0 │ add_15[0][0] │ │ (GlobalAveragePool… │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dense_4 (Dense) │ (None, 128) │ 1,152 │ global_average_p… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dropout_25 │ (None, 128) │ 0 │ dense_4[0][0] │ │ (Dropout) │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dense_5 (Dense) │ (None, 1) │ 129 │ dropout_25[0][0] │ └─────────────────────┴───────────────────┴────────────┴───────────────────┘
Total params: 145,161 (567.04 KB)
Trainable params: 145,161 (567.04 KB)
Non-trainable params: 0 (0.00 B)
Finalmente, compilamos el modelo y lo entrenamos:
model.compile(
loss=MeanSquaredError(), optimizer=Adam(learning_rate=1e-4), metrics=[MeanAbsoluteError()]
)
history = model.fit(X_train, y_train, validation_split=0.2, epochs=500, batch_size=64, callbacks=[EarlyStopping(patience=10, restore_best_weights=True)])
Epoch 1/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 6s 118ms/step - loss: 3745746176.0000 - mean_absolute_error: 59417.3438 - val_loss: 7450994176.0000 - val_mean_absolute_error: 86252.1484 Epoch 2/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 3584442624.0000 - mean_absolute_error: 58189.7344 - val_loss: 7083529216.0000 - val_mean_absolute_error: 84096.5781 Epoch 3/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 3497890816.0000 - mean_absolute_error: 57348.5156 - val_loss: 6730481152.0000 - val_mean_absolute_error: 81972.1953 Epoch 4/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 3434803968.0000 - mean_absolute_error: 56559.7656 - val_loss: 6389161984.0000 - val_mean_absolute_error: 79864.6641 Epoch 5/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 3237461248.0000 - mean_absolute_error: 54969.1289 - val_loss: 6057072640.0000 - val_mean_absolute_error: 77759.3047 Epoch 6/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 2980279040.0000 - mean_absolute_error: 53075.0625 - val_loss: 5737446912.0000 - val_mean_absolute_error: 75677.6484 Epoch 7/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 2947464448.0000 - mean_absolute_error: 52717.8008 - val_loss: 5429929984.0000 - val_mean_absolute_error: 73619.3047 Epoch 8/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 2773769216.0000 - mean_absolute_error: 50736.4922 - val_loss: 5131864576.0000 - val_mean_absolute_error: 71567.7188 Epoch 9/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 2629158400.0000 - mean_absolute_error: 49513.3906 - val_loss: 4847659008.0000 - val_mean_absolute_error: 69555.1797 Epoch 10/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 2413897472.0000 - mean_absolute_error: 47151.4688 - val_loss: 4576677376.0000 - val_mean_absolute_error: 67580.4766 Epoch 11/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 2280071680.0000 - mean_absolute_error: 45747.7148 - val_loss: 4315822080.0000 - val_mean_absolute_error: 65623.4297 Epoch 12/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 2034020480.0000 - mean_absolute_error: 43200.4805 - val_loss: 4066002944.0000 - val_mean_absolute_error: 63692.8242 Epoch 13/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 2098339840.0000 - mean_absolute_error: 43711.4805 - val_loss: 3825202176.0000 - val_mean_absolute_error: 61774.8047 Epoch 14/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 2030695168.0000 - mean_absolute_error: 42896.8516 - val_loss: 3593901568.0000 - val_mean_absolute_error: 59874.6055 Epoch 15/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 1925586944.0000 - mean_absolute_error: 41686.7305 - val_loss: 3370589440.0000 - val_mean_absolute_error: 57980.9570 Epoch 16/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 35ms/step - loss: 1681312512.0000 - mean_absolute_error: 38611.3711 - val_loss: 3158381824.0000 - val_mean_absolute_error: 56122.2773 Epoch 17/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 1749163776.0000 - mean_absolute_error: 39584.3555 - val_loss: 2954106368.0000 - val_mean_absolute_error: 54272.9453 Epoch 18/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 1555701632.0000 - mean_absolute_error: 36849.6367 - val_loss: 2760908800.0000 - val_mean_absolute_error: 52463.9297 Epoch 19/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 1527947776.0000 - mean_absolute_error: 36754.5078 - val_loss: 2579151104.0000 - val_mean_absolute_error: 50703.1211 Epoch 20/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 36ms/step - loss: 1221455488.0000 - mean_absolute_error: 32778.7656 - val_loss: 2407470336.0000 - val_mean_absolute_error: 48981.8047 Epoch 21/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 1278507648.0000 - mean_absolute_error: 33169.1719 - val_loss: 2243867392.0000 - val_mean_absolute_error: 47283.1758 Epoch 22/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 1203534848.0000 - mean_absolute_error: 32092.0684 - val_loss: 2090037248.0000 - val_mean_absolute_error: 45628.3398 Epoch 23/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 1188379904.0000 - mean_absolute_error: 32024.5742 - val_loss: 1943131520.0000 - val_mean_absolute_error: 43989.8867 Epoch 24/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 1117707904.0000 - mean_absolute_error: 30603.5430 - val_loss: 1802948992.0000 - val_mean_absolute_error: 42367.3594 Epoch 25/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 937231488.0000 - mean_absolute_error: 27950.3223 - val_loss: 1672542336.0000 - val_mean_absolute_error: 40800.0508 Epoch 26/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 951918080.0000 - mean_absolute_error: 28098.0059 - val_loss: 1548963840.0000 - val_mean_absolute_error: 39257.0625 Epoch 27/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 887416768.0000 - mean_absolute_error: 26882.8887 - val_loss: 1431657600.0000 - val_mean_absolute_error: 37734.0391 Epoch 28/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 825235264.0000 - mean_absolute_error: 25308.2129 - val_loss: 1322041984.0000 - val_mean_absolute_error: 36253.0391 Epoch 29/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 710710848.0000 - mean_absolute_error: 24010.3594 - val_loss: 1219309312.0000 - val_mean_absolute_error: 34807.8398 Epoch 30/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 735455616.0000 - mean_absolute_error: 23514.9395 - val_loss: 1123308672.0000 - val_mean_absolute_error: 33400.8359 Epoch 31/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 706120704.0000 - mean_absolute_error: 23163.1738 - val_loss: 1031689856.0000 - val_mean_absolute_error: 32000.3652 Epoch 32/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 656734656.0000 - mean_absolute_error: 22572.3359 - val_loss: 945756160.0000 - val_mean_absolute_error: 30628.6152 Epoch 33/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 572414976.0000 - mean_absolute_error: 20504.5391 - val_loss: 867117760.0000 - val_mean_absolute_error: 29317.0840 Epoch 34/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 583515264.0000 - mean_absolute_error: 20375.3789 - val_loss: 794034816.0000 - val_mean_absolute_error: 28043.2246 Epoch 35/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 35ms/step - loss: 513413408.0000 - mean_absolute_error: 19304.4297 - val_loss: 727099968.0000 - val_mean_absolute_error: 26823.4570 Epoch 36/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 536967936.0000 - mean_absolute_error: 19707.5449 - val_loss: 663707648.0000 - val_mean_absolute_error: 25614.7051 Epoch 37/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 516474112.0000 - mean_absolute_error: 19139.6387 - val_loss: 604453056.0000 - val_mean_absolute_error: 24430.7773 Epoch 38/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 463176352.0000 - mean_absolute_error: 17964.5391 - val_loss: 549185152.0000 - val_mean_absolute_error: 23272.2246 Epoch 39/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 35ms/step - loss: 428581280.0000 - mean_absolute_error: 16835.9863 - val_loss: 500371872.0000 - val_mean_absolute_error: 22198.7012 Epoch 40/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 471184800.0000 - mean_absolute_error: 17893.6035 - val_loss: 454952448.0000 - val_mean_absolute_error: 21150.8809 Epoch 41/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 387471680.0000 - mean_absolute_error: 16218.2500 - val_loss: 412156832.0000 - val_mean_absolute_error: 20113.6523 Epoch 42/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 391487872.0000 - mean_absolute_error: 16147.4570 - val_loss: 372006016.0000 - val_mean_absolute_error: 19089.2852 Epoch 43/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 318778880.0000 - mean_absolute_error: 14805.4160 - val_loss: 335636896.0000 - val_mean_absolute_error: 18111.4004 Epoch 44/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 388663456.0000 - mean_absolute_error: 15901.3281 - val_loss: 302760384.0000 - val_mean_absolute_error: 17179.5254 Epoch 45/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 35ms/step - loss: 306657344.0000 - mean_absolute_error: 13851.1123 - val_loss: 273251104.0000 - val_mean_absolute_error: 16297.7168 Epoch 46/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 308065504.0000 - mean_absolute_error: 14001.3564 - val_loss: 246676528.0000 - val_mean_absolute_error: 15460.5576 Epoch 47/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 308488480.0000 - mean_absolute_error: 14078.1055 - val_loss: 222193184.0000 - val_mean_absolute_error: 14646.9229 Epoch 48/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 301534048.0000 - mean_absolute_error: 14092.3838 - val_loss: 198663632.0000 - val_mean_absolute_error: 13819.8438 Epoch 49/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 35ms/step - loss: 297777856.0000 - mean_absolute_error: 13324.6445 - val_loss: 177539744.0000 - val_mean_absolute_error: 13032.6113 Epoch 50/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 291699904.0000 - mean_absolute_error: 13837.0674 - val_loss: 159391856.0000 - val_mean_absolute_error: 12316.0928 Epoch 51/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 283361952.0000 - mean_absolute_error: 12941.7227 - val_loss: 142683504.0000 - val_mean_absolute_error: 11617.3242 Epoch 52/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 260518496.0000 - mean_absolute_error: 12792.4844 - val_loss: 127451008.0000 - val_mean_absolute_error: 10941.3789 Epoch 53/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 226388768.0000 - mean_absolute_error: 11651.0693 - val_loss: 114276560.0000 - val_mean_absolute_error: 10321.0391 Epoch 54/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 205382560.0000 - mean_absolute_error: 11351.9678 - val_loss: 103098872.0000 - val_mean_absolute_error: 9763.7920 Epoch 55/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 35ms/step - loss: 275552608.0000 - mean_absolute_error: 13087.9854 - val_loss: 92407920.0000 - val_mean_absolute_error: 9199.2021 Epoch 56/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 268273168.0000 - mean_absolute_error: 12543.3955 - val_loss: 82048792.0000 - val_mean_absolute_error: 8616.8262 Epoch 57/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 38ms/step - loss: 220480832.0000 - mean_absolute_error: 11706.5801 - val_loss: 72859072.0000 - val_mean_absolute_error: 8064.9683 Epoch 58/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 224087952.0000 - mean_absolute_error: 11864.0859 - val_loss: 65224648.0000 - val_mean_absolute_error: 7575.9087 Epoch 59/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 235802848.0000 - mean_absolute_error: 12139.7373 - val_loss: 58223352.0000 - val_mean_absolute_error: 7097.7500 Epoch 60/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 212309136.0000 - mean_absolute_error: 11709.4404 - val_loss: 51337644.0000 - val_mean_absolute_error: 6593.6338 Epoch 61/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 203259824.0000 - mean_absolute_error: 11343.0186 - val_loss: 45863832.0000 - val_mean_absolute_error: 6163.4277 Epoch 62/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 223067424.0000 - mean_absolute_error: 11803.2500 - val_loss: 41232888.0000 - val_mean_absolute_error: 5775.6211 Epoch 63/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 214825680.0000 - mean_absolute_error: 11669.9521 - val_loss: 37446536.0000 - val_mean_absolute_error: 5450.4863 Epoch 64/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 216364576.0000 - mean_absolute_error: 11809.4834 - val_loss: 34309524.0000 - val_mean_absolute_error: 5169.9111 Epoch 65/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 235546656.0000 - mean_absolute_error: 12177.5908 - val_loss: 31150866.0000 - val_mean_absolute_error: 4869.8384 Epoch 66/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 35ms/step - loss: 239074352.0000 - mean_absolute_error: 11923.0518 - val_loss: 28446814.0000 - val_mean_absolute_error: 4605.3867 Epoch 67/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 199416336.0000 - mean_absolute_error: 11411.8555 - val_loss: 25861126.0000 - val_mean_absolute_error: 4349.9116 Epoch 68/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 248116576.0000 - mean_absolute_error: 11959.5605 - val_loss: 23593448.0000 - val_mean_absolute_error: 4114.2622 Epoch 69/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 210143584.0000 - mean_absolute_error: 11338.5674 - val_loss: 21488612.0000 - val_mean_absolute_error: 3885.1150 Epoch 70/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 221852560.0000 - mean_absolute_error: 11754.5479 - val_loss: 19523164.0000 - val_mean_absolute_error: 3662.5378 Epoch 71/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 192888768.0000 - mean_absolute_error: 10886.8115 - val_loss: 17984884.0000 - val_mean_absolute_error: 3489.9468 Epoch 72/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 235641520.0000 - mean_absolute_error: 12318.1562 - val_loss: 16982524.0000 - val_mean_absolute_error: 3373.4075 Epoch 73/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 174572480.0000 - mean_absolute_error: 10311.3486 - val_loss: 16302368.0000 - val_mean_absolute_error: 3293.4270 Epoch 74/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 187568832.0000 - mean_absolute_error: 10602.3965 - val_loss: 15533554.0000 - val_mean_absolute_error: 3203.0239 Epoch 75/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 198057216.0000 - mean_absolute_error: 11358.1084 - val_loss: 14863178.0000 - val_mean_absolute_error: 3121.3547 Epoch 76/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 195985968.0000 - mean_absolute_error: 11176.5371 - val_loss: 14400610.0000 - val_mean_absolute_error: 3064.3596 Epoch 77/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 35ms/step - loss: 190361584.0000 - mean_absolute_error: 11096.1230 - val_loss: 13723885.0000 - val_mean_absolute_error: 2978.9697 Epoch 78/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 215974176.0000 - mean_absolute_error: 11691.9307 - val_loss: 12993327.0000 - val_mean_absolute_error: 2883.9270 Epoch 79/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 190197344.0000 - mean_absolute_error: 10565.8652 - val_loss: 12610756.0000 - val_mean_absolute_error: 2833.9299 Epoch 80/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 212198656.0000 - mean_absolute_error: 11182.1338 - val_loss: 12257129.0000 - val_mean_absolute_error: 2789.1084 Epoch 81/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 242291824.0000 - mean_absolute_error: 12242.4814 - val_loss: 11830018.0000 - val_mean_absolute_error: 2736.3418 Epoch 82/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 177391504.0000 - mean_absolute_error: 10681.7812 - val_loss: 11522535.0000 - val_mean_absolute_error: 2696.6550 Epoch 83/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 189170992.0000 - mean_absolute_error: 11077.2529 - val_loss: 11556202.0000 - val_mean_absolute_error: 2701.0859 Epoch 84/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 254000624.0000 - mean_absolute_error: 12410.7002 - val_loss: 11573036.0000 - val_mean_absolute_error: 2703.2935 Epoch 85/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 223350768.0000 - mean_absolute_error: 11223.1445 - val_loss: 11635862.0000 - val_mean_absolute_error: 2711.4849 Epoch 86/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 207653744.0000 - mean_absolute_error: 11298.6719 - val_loss: 11664439.0000 - val_mean_absolute_error: 2715.1860 Epoch 87/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 213104144.0000 - mean_absolute_error: 11679.9219 - val_loss: 11515890.0000 - val_mean_absolute_error: 2695.7795 Epoch 88/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 37ms/step - loss: 187125872.0000 - mean_absolute_error: 11162.5049 - val_loss: 11464537.0000 - val_mean_absolute_error: 2688.9736 Epoch 89/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 39ms/step - loss: 198384848.0000 - mean_absolute_error: 11137.9414 - val_loss: 11615012.0000 - val_mean_absolute_error: 2708.7769 Epoch 90/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 36ms/step - loss: 206628048.0000 - mean_absolute_error: 11516.2695 - val_loss: 11428503.0000 - val_mean_absolute_error: 2684.3733 Epoch 91/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 35ms/step - loss: 238605376.0000 - mean_absolute_error: 11818.2432 - val_loss: 11304992.0000 - val_mean_absolute_error: 2668.7141 Epoch 92/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 205361744.0000 - mean_absolute_error: 11397.6826 - val_loss: 10985944.0000 - val_mean_absolute_error: 2627.8962 Epoch 93/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 35ms/step - loss: 211178928.0000 - mean_absolute_error: 11291.4756 - val_loss: 10653610.0000 - val_mean_absolute_error: 2588.5466 Epoch 94/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 176058352.0000 - mean_absolute_error: 10503.4014 - val_loss: 10445253.0000 - val_mean_absolute_error: 2563.6931 Epoch 95/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 35ms/step - loss: 193308672.0000 - mean_absolute_error: 10753.0684 - val_loss: 10176836.0000 - val_mean_absolute_error: 2529.9675 Epoch 96/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 190639872.0000 - mean_absolute_error: 11028.9736 - val_loss: 9975597.0000 - val_mean_absolute_error: 2503.9451 Epoch 97/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 213449568.0000 - mean_absolute_error: 11471.3047 - val_loss: 9970602.0000 - val_mean_absolute_error: 2503.3164 Epoch 98/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 214516336.0000 - mean_absolute_error: 11375.8623 - val_loss: 10080932.0000 - val_mean_absolute_error: 2517.3918 Epoch 99/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 184926304.0000 - mean_absolute_error: 11044.6748 - val_loss: 10170070.0000 - val_mean_absolute_error: 2529.0974 Epoch 100/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 177275120.0000 - mean_absolute_error: 10438.5527 - val_loss: 10346198.0000 - val_mean_absolute_error: 2551.4980 Epoch 101/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 182478080.0000 - mean_absolute_error: 10725.0342 - val_loss: 10483605.0000 - val_mean_absolute_error: 2568.3667 Epoch 102/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 193579648.0000 - mean_absolute_error: 11183.3711 - val_loss: 10561116.0000 - val_mean_absolute_error: 2577.6641 Epoch 103/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 37ms/step - loss: 191002992.0000 - mean_absolute_error: 11364.5166 - val_loss: 10642569.0000 - val_mean_absolute_error: 2587.2781 Epoch 104/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 32ms/step - loss: 232144416.0000 - mean_absolute_error: 12066.3174 - val_loss: 10696481.0000 - val_mean_absolute_error: 2593.5569 Epoch 105/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 205302496.0000 - mean_absolute_error: 11548.7861 - val_loss: 10731261.0000 - val_mean_absolute_error: 2597.5745 Epoch 106/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step - loss: 197727152.0000 - mean_absolute_error: 11232.9912 - val_loss: 10733767.0000 - val_mean_absolute_error: 2597.8652 Epoch 107/500 5/5 ━━━━━━━━━━━━━━━━━━━━ 0s 34ms/step - loss: 205110688.0000 - mean_absolute_error: 11341.7080 - val_loss: 10678499.0000 - val_mean_absolute_error: 2591.4771
Mostramos el progreso del entrenamiento:
plot_loss(history, "resources/embedding_model_loss.jpeg")
Evaluamos el modelo en el conjunto de test:
test_loss = model.evaluate(X_test, y_test, verbose=1, return_dict=True)
pprint(test_loss)
2/2 ━━━━━━━━━━━━━━━━━━━━ 0s 20ms/step - loss: 13550875.0000 - mean_absolute_error: 2856.5977 2/2 ━━━━━━━━━━━━━━━━━━━━ 0s 20ms/step - loss: 13550875.0000 - mean_absolute_error: 2856.5977 {'loss': 13550875.0, 'mean_absolute_error': 2856.59765625}
Calculamos las métricas de evaluación:
pred_test = model.predict(X_test)
metrics_dict_test = calculate_metrics(y_test, pred_test)
print("\nTest Set Metrics:")
for metric_name, metric_value in metrics_dict_test.items():
print(f"Score ({metric_name}): {metric_value}")
2/2 ━━━━━━━━━━━━━━━━━━━━ 1s 257ms/step Test Set Metrics: Score (RMSE): 3681.151605436321 Score (MAPE): 3.3357610550518024 Score (MAE): 2856.598098214285 Score (MSE): 13550877.142206404 Score (sMAPE): 3.3292887925528225 Score (RRMSE): 4.323217428686552 Score (Quantile Loss): 1428.2990491071425
Guardamos las métricas de evaluación en el diccionario:
df_line = pd.DataFrame({
"model": "CustTransfEmb",
"mae": metrics_dict_test["MAE"],
"mape": metrics_dict_test["MAPE"],
"rmse": metrics_dict_test["RMSE"]
}, index=[0])
df_metrics = pd.concat([df_metrics, df_line], ignore_index=True)
df_metrics
| model | mae | mape | rmse | |
|---|---|---|---|---|
| 0 | Naive | 1735.726500 | 1.888647 | 2434.504211 |
| 1 | ARIMA(1,1,35) | 1891.662281 | 2.063588 | 2546.257391 |
| 2 | ARIMA(0,1,0) | 1735.726500 | 1.894494 | 2434.504211 |
| 3 | Prophet | 2582.551322 | 2.871448 | 3621.703076 |
| 4 | XGBoost | 4319.031819 | 4.609317 | 5555.304516 |
| 5 | LSTM | 2432.965561 | 2.645095 | 3192.781096 |
| 6 | CustTransf | 2437.519884 | 2.837959 | 3324.778151 |
| 7 | CustTransfEmb | 2856.598098 | 3.335761 | 3681.151605 |
Realizamos la predicción y graficamos los resultados:
plot_actual_vs_predicted(y_test, pred_test, "resources/embbeding_model_predict.jpeg")
4. Estado del arte ¶
4.1. Caso de uso: TimesFM ¶
En esta sección, se presenta el análisis de la serie que se viene trabajando pero con el modelo TimesFM. Se utiliza TimesFmModelForPrediction de HuggingFace.
TimesFmModelForPrediction espera:
- $\rightarrow$
past_values: Una lista de tensores, donde cada tensor representa una serie temporal individual. El modelo está diseñado para pronosticar múltiples series temporales a la vez, por lo que past_values es una lista. Cada tensor en la lista debe tener la forma(sequence_length,). - $\rightarrow$
freq: Una lista (o un tensor) de índices de frecuencia, uno para cada serie temporal en past_values.TimesFMutiliza estas frecuencias para adaptar su codificación posicional y otros aspectos del modelo a la periodicidad de los datos. Los valores típicos son:- $0$: Alta frecuencia (ej. diario, horario).
- $1$: Frecuencia media (ej. semanal, mensual).
- $2$: Baja frecuencia (ej. trimestral, anual).
En el ejemplo de HuggingFace, se utilizan estos datos como prueba:
# Create dummy inputs
forecast_input = [
np.sin(np.linspace(0, 20, 100)),
np.sin(np.linspace(0, 20, 200)),
np.sin(np.linspace(0, 20, 400)),
]
frequency_input = [0, 1, 2]
# Convert inputs to sequence of tensors
forecast_input_tensor = [
torch.tensor(ts, dtype=torch.bfloat16).to("cuda" if torch.cuda.is_available() else "cpu") for ts in forecast_input
]
frequency_input_tensor = torch.tensor(frequency_input, dtype=torch.long).to(
"cuda" if torch.cuda.is_available() else "cpu"
)
En donde podemos observar que forecast_input es una lista de series temporales, y frequency_input es una lista de frecuencias asociadas a cada serie temporal.
Para este caso, el conjunto spots_train y spots_test ya tienen la forma adecuada (serie univariada), por lo que simplemente debemos crear la lista de tensores y las frecuencias asociadas. A diferencia de los casos anteriores, en donde se formateaba el conjunto para una ventana de tiempo.
Convertimos los datos a tensores:
forecast_input = np.array(spots_train).flatten()
Creamos los tensores y lo tiramos a la GPU si está disponible:
forecast_input_tensor = [torch.tensor(forecast_input, dtype=torch.bfloat16).to(DEVICE)]
frequency_input_tensor = torch.tensor([0], dtype=torch.long).to(DEVICE)
print(f"Input tensor shape: {forecast_input_tensor[0].shape}")
print(f"Frequency tensor shape: {frequency_input_tensor.shape}")
Input tensor shape: torch.Size([356]) Frequency tensor shape: torch.Size([1])
Cargamos el modelo:
model = TimesFmModelForPrediction.from_pretrained(
"google/timesfm-2.0-500m-pytorch",
torch_dtype=torch.bfloat16,
attn_implementation="sdpa",
device_map=DEVICE,
)
config = model.config
Imprimimos el resumen del modelo:
print(model)
TimesFmModelForPrediction(
(decoder): TimesFmModel(
(input_ff_layer): TimesFmResidualBlock(
(input_layer): Linear(in_features=64, out_features=1280, bias=True)
(activation): SiLU()
(output_layer): Linear(in_features=1280, out_features=1280, bias=True)
(residual_layer): Linear(in_features=64, out_features=1280, bias=True)
)
(freq_emb): Embedding(3, 1280)
(layers): ModuleList(
(0-49): 50 x TimesFmDecoderLayer(
(self_attn): TimesFmAttention(
(q_proj): Linear(in_features=1280, out_features=1280, bias=True)
(k_proj): Linear(in_features=1280, out_features=1280, bias=True)
(v_proj): Linear(in_features=1280, out_features=1280, bias=True)
(o_proj): Linear(in_features=1280, out_features=1280, bias=True)
)
(mlp): TimesFmMLP(
(gate_proj): Linear(in_features=1280, out_features=1280, bias=True)
(down_proj): Linear(in_features=1280, out_features=1280, bias=True)
(layer_norm): LayerNorm((1280,), eps=1e-06, elementwise_affine=True)
)
(input_layernorm): TimesFmRMSNorm((1280,), eps=1e-06)
)
)
)
(horizon_ff_layer): TimesFmResidualBlock(
(input_layer): Linear(in_features=1280, out_features=1280, bias=True)
(activation): SiLU()
(output_layer): Linear(in_features=1280, out_features=1280, bias=True)
(residual_layer): Linear(in_features=1280, out_features=1280, bias=True)
)
)
Mostramos la configuración del modelo:
print(config)
TimesFmConfig {
"architectures": [
"TimesFmModelForPrediction"
],
"attention_dropout": 0.0,
"context_length": 2048,
"freq_size": 3,
"head_dim": 80,
"hidden_size": 1280,
"horizon_length": 128,
"initializer_range": 0.02,
"intermediate_size": 1280,
"max_timescale": 10000,
"min_timescale": 1,
"model_type": "timesfm",
"num_attention_heads": 16,
"num_hidden_layers": 50,
"pad_val": 1123581321.0,
"patch_length": 32,
"quantiles": [
0.1,
0.2,
0.3,
0.4,
0.5,
0.6,
0.7,
0.8,
0.9
],
"rms_norm_eps": 1e-06,
"tolerance": 1e-06,
"torch_dtype": "bfloat16",
"transformers_version": "4.52.4",
"use_positional_embedding": false
}
💡 Idea: Observamos que el modelo tiene un horizonte de predicción de 128 pasos, por lo que está contemplado nuestro conjunto de test, que tiene cerca de 40 pasos (dias):
assert config.horizon_length > len(spots_test)
Realizamos la predicción:
# Get predictions from the pre-trained model
with torch.no_grad():
outputs = model(past_values=forecast_input_tensor, freq=frequency_input_tensor, return_dict=True)
point_forecast_conv = outputs.mean_predictions.float().cpu().numpy()
quantile_forecast_conv = outputs.full_predictions.float().cpu().numpy()
# Print the shape of the predictions
print("Point Forecast Shape:", point_forecast_conv.shape)
print("Quantile Forecast Shape:", quantile_forecast_conv.shape)
Point Forecast Shape: (1, 128) Quantile Forecast Shape: (1, 128, 10)
Finalmente, truncamos la predicción a los primeros 40 pasos y procesamos los resultados:
y_true = np.array(spots_test).flatten()
pred_test = point_forecast_conv.flatten()[:len(y_true)]
Calculamos las métricas de evaluación:
metrics_dict_test = calculate_metrics(y_true, pred_test)
print("\nTest Set Metrics:")
for metric_name, metric_value in metrics_dict_test.items():
print(f"Score ({metric_name}): {metric_value}")
Test Set Metrics: Score (RMSE): 3870.8257111206262 Score (MAPE): 3.819422342604954 Score (MAE): 3279.7782500000003 Score (MSE): 14983291.6858725 Score (sMAPE): 3.777163122355528 Score (RRMSE): 4.476180061450597 Score (Quantile Loss): 1639.8891250000001
Guardamos las métricas de evaluación en el diccionario:
df_line = pd.DataFrame({
"model": "TimesFM",
"mae": metrics_dict_test["MAE"],
"mape": metrics_dict_test["MAPE"],
"rmse": metrics_dict_test["RMSE"]
}, index=[0])
df_metrics = pd.concat([df_metrics, df_line], ignore_index=True)
df_metrics
| model | mae | mape | rmse | |
|---|---|---|---|---|
| 0 | Naive | 1735.726500 | 1.888647 | 2434.504211 |
| 1 | ARIMA(1,1,35) | 1891.662281 | 2.063588 | 2546.257391 |
| 2 | ARIMA(0,1,0) | 1735.726500 | 1.894494 | 2434.504211 |
| 3 | Prophet | 2582.551322 | 2.871448 | 3621.703076 |
| 4 | XGBoost | 4319.031819 | 4.609317 | 5555.304516 |
| 5 | LSTM | 2432.965561 | 2.645095 | 3192.781096 |
| 6 | CustTransf | 2437.519884 | 2.837959 | 3324.778151 |
| 7 | CustTransfEmb | 2856.598098 | 3.335761 | 3681.151605 |
| 8 | TimesFM | 3279.778250 | 3.819422 | 3870.825711 |
Graficamos los resultados:
plot_actual_vs_predicted(y_test, pred_test, "resources/timesfm_model_predict.jpeg")
4.2. Caso de uso: PatchTST ¶
# --- Definir la clase Dataset para series temporales ---
class TimeSeriesDataset(Dataset):
def __init__(self, data, context_length, forecast_horizon):
self.data = data
self.context_length = context_length
self.forecast_horizon = forecast_horizon
def __len__(self):
return max(0, len(self.data) - self.context_length - self.forecast_horizon)
def __getitem__(self, idx):
x = self.data[idx : idx + self.context_length]
y = self.data[idx + self.context_length : idx + self.context_length + self.forecast_horizon]
return torch.tensor(x, dtype=torch.float32), torch.tensor(y, dtype=torch.float32)
# --- Parámetros ---
context_length = 24
forecast_horizon = 1
batch_size = 16
# --- Normalizar la serie ---
scaler = StandardScaler()
df['valor_normalizado'] = scaler.fit_transform(df[['Close']])
# --- Extraer la serie normalizada ---
series = df['valor_normalizado'].values
# --- Dividir en train y test ---
N = len(series)
train_size = int(N * 0.9)
train_series = series[:train_size]
test_series = series[train_size:]
print(f"Tamaño total: {N}, Train: {len(train_series)}, Test: {len(test_series)}")
# --- Crear datasets ---
train_dataset = TimeSeriesDataset(train_series, context_length, forecast_horizon)
test_dataset = TimeSeriesDataset(test_series, context_length, forecast_horizon)
print(f"Ejemplos train: {len(train_dataset)}, Ejemplos test: {len(test_dataset)}")
# --- Crear DataLoaders ---
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
# Ahora train_loader y test_loader están listos para usar en entrenamiento y evaluación
Tamaño total: 396, Train: 356, Test: 40 Ejemplos train: 331, Ejemplos test: 15
# Crear la configuración del modelo
config = PatchTSTConfig(
context_length=24,
prediction_length=12,
d_model=64,
num_input_channels=1, # Univariada
num_targets=1,
patch_length=12,
patch_stride=6,
num_hidden_layers=1,
num_attention_heads=4,
dropout=0.1
)
# Instanciar el modelo
model = PatchTSTForPrediction(config)
# --- Parámetros ---
num_epochs = 50
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# --- Mover modelo a dispositivo ---
model = model.to(device)
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
train_losses = []
test_losses = []
for epoch in range(num_epochs):
model.train()
train_loss = 0.0
for x_batch, y_batch in train_loader:
x_batch = x_batch.to(device).unsqueeze(-1)
y_batch = y_batch.to(device)
optimizer.zero_grad()
output = model(past_values=x_batch)
preds = output.prediction_outputs.squeeze(-1)
loss = criterion(preds, y_batch)
loss.backward()
optimizer.step()
train_loss += loss.item() * x_batch.size(0)
train_loss /= len(train_loader.dataset)
train_losses.append(train_loss)
model.eval()
test_loss = 0.0
with torch.no_grad():
for x_batch, y_batch in test_loader:
x_batch = x_batch.to(device).unsqueeze(-1)
y_batch = y_batch.to(device)
output = model(past_values=x_batch)
preds = output.prediction_outputs.squeeze(-1)
loss = criterion(preds, y_batch)
test_loss += loss.item() * x_batch.size(0)
test_loss /= len(test_loader.dataset)
test_losses.append(test_loss)
print(f"Epoch {epoch+1}/{num_epochs} | Train Loss: {train_loss:.4f} | Test Loss: {test_loss:.4f}")
# Graficar la evolución del error
plt.figure(figsize=(8,5))
plt.plot(range(1, num_epochs+1), train_losses, label='Train Loss')
plt.plot(range(1, num_epochs+1), test_losses, label='Test Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss (MSE)')
plt.title('Evolución del error durante el entrenamiento')
plt.legend()
plt.grid(True)
plt.show()
e:\Documentos\Git Repositories\uba-mia-ast2\.venv\Lib\site-packages\torch\nn\modules\loss.py:610: UserWarning: Using a target size (torch.Size([16, 1])) that is different to the input size (torch.Size([16, 12])). This will likely lead to incorrect results due to broadcasting. Please ensure they have the same size. return F.mse_loss(input, target, reduction=self.reduction) e:\Documentos\Git Repositories\uba-mia-ast2\.venv\Lib\site-packages\torch\nn\modules\loss.py:610: UserWarning: Using a target size (torch.Size([11, 1])) that is different to the input size (torch.Size([11, 12])). This will likely lead to incorrect results due to broadcasting. Please ensure they have the same size. return F.mse_loss(input, target, reduction=self.reduction) e:\Documentos\Git Repositories\uba-mia-ast2\.venv\Lib\site-packages\torch\nn\modules\loss.py:610: UserWarning: Using a target size (torch.Size([15, 1])) that is different to the input size (torch.Size([15, 12])). This will likely lead to incorrect results due to broadcasting. Please ensure they have the same size. return F.mse_loss(input, target, reduction=self.reduction)
Epoch 1/50 | Train Loss: 0.1131 | Test Loss: 0.0242 Epoch 2/50 | Train Loss: 0.1048 | Test Loss: 0.0227 Epoch 3/50 | Train Loss: 0.0805 | Test Loss: 0.0185 Epoch 4/50 | Train Loss: 0.0486 | Test Loss: 0.0300 Epoch 5/50 | Train Loss: 0.0310 | Test Loss: 0.0267 Epoch 6/50 | Train Loss: 0.0285 | Test Loss: 0.0333 Epoch 7/50 | Train Loss: 0.0283 | Test Loss: 0.0294 Epoch 8/50 | Train Loss: 0.0249 | Test Loss: 0.0307 Epoch 9/50 | Train Loss: 0.0250 | Test Loss: 0.0299 Epoch 10/50 | Train Loss: 0.0241 | Test Loss: 0.0314 Epoch 11/50 | Train Loss: 0.0258 | Test Loss: 0.0306 Epoch 12/50 | Train Loss: 0.0235 | Test Loss: 0.0292 Epoch 13/50 | Train Loss: 0.0275 | Test Loss: 0.0267 Epoch 14/50 | Train Loss: 0.0255 | Test Loss: 0.0270 Epoch 15/50 | Train Loss: 0.0216 | Test Loss: 0.0278 Epoch 16/50 | Train Loss: 0.0272 | Test Loss: 0.0263 Epoch 17/50 | Train Loss: 0.0229 | Test Loss: 0.0285 Epoch 18/50 | Train Loss: 0.0235 | Test Loss: 0.0294 Epoch 19/50 | Train Loss: 0.0216 | Test Loss: 0.0292 Epoch 20/50 | Train Loss: 0.0196 | Test Loss: 0.0267 Epoch 21/50 | Train Loss: 0.0211 | Test Loss: 0.0293 Epoch 22/50 | Train Loss: 0.0188 | Test Loss: 0.0284 Epoch 23/50 | Train Loss: 0.0222 | Test Loss: 0.0265 Epoch 24/50 | Train Loss: 0.0194 | Test Loss: 0.0272 Epoch 25/50 | Train Loss: 0.0210 | Test Loss: 0.0274 Epoch 26/50 | Train Loss: 0.0238 | Test Loss: 0.0287 Epoch 27/50 | Train Loss: 0.0220 | Test Loss: 0.0288 Epoch 28/50 | Train Loss: 0.0205 | Test Loss: 0.0301 Epoch 29/50 | Train Loss: 0.0221 | Test Loss: 0.0296 Epoch 30/50 | Train Loss: 0.0209 | Test Loss: 0.0282 Epoch 31/50 | Train Loss: 0.0199 | Test Loss: 0.0278 Epoch 32/50 | Train Loss: 0.0223 | Test Loss: 0.0297 Epoch 33/50 | Train Loss: 0.0225 | Test Loss: 0.0281 Epoch 34/50 | Train Loss: 0.0201 | Test Loss: 0.0287 Epoch 35/50 | Train Loss: 0.0243 | Test Loss: 0.0283 Epoch 36/50 | Train Loss: 0.0235 | Test Loss: 0.0278 Epoch 37/50 | Train Loss: 0.0237 | Test Loss: 0.0271 Epoch 38/50 | Train Loss: 0.0199 | Test Loss: 0.0285 Epoch 39/50 | Train Loss: 0.0225 | Test Loss: 0.0277 Epoch 40/50 | Train Loss: 0.0194 | Test Loss: 0.0286 Epoch 41/50 | Train Loss: 0.0204 | Test Loss: 0.0278 Epoch 42/50 | Train Loss: 0.0207 | Test Loss: 0.0300 Epoch 43/50 | Train Loss: 0.0213 | Test Loss: 0.0297 Epoch 44/50 | Train Loss: 0.0230 | Test Loss: 0.0301 Epoch 45/50 | Train Loss: 0.0188 | Test Loss: 0.0293 Epoch 46/50 | Train Loss: 0.0195 | Test Loss: 0.0293 Epoch 47/50 | Train Loss: 0.0199 | Test Loss: 0.0315 Epoch 48/50 | Train Loss: 0.0186 | Test Loss: 0.0331 Epoch 49/50 | Train Loss: 0.0195 | Test Loss: 0.0305 Epoch 50/50 | Train Loss: 0.0247 | Test Loss: 0.0306
torch.save(model, "patchtst_model_complete.pth")
model = torch.load("patchtst_model_complete.pth", weights_only=False)
model.to(device)
model.eval()
PatchTSTForPrediction(
(model): PatchTSTModel(
(scaler): PatchTSTScaler(
(scaler): PatchTSTStdScaler()
)
(patchifier): PatchTSTPatchify()
(masking): Identity()
(encoder): PatchTSTEncoder(
(embedder): PatchTSTEmbedding(
(input_embedding): Linear(in_features=12, out_features=64, bias=True)
)
(positional_encoder): PatchTSTPositionalEncoding(
(positional_dropout): Identity()
)
(layers): ModuleList(
(0): PatchTSTEncoderLayer(
(self_attn): PatchTSTAttention(
(k_proj): Linear(in_features=64, out_features=64, bias=True)
(v_proj): Linear(in_features=64, out_features=64, bias=True)
(q_proj): Linear(in_features=64, out_features=64, bias=True)
(out_proj): Linear(in_features=64, out_features=64, bias=True)
)
(dropout_path1): Identity()
(norm_sublayer1): PatchTSTBatchNorm(
(batchnorm): BatchNorm1d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(ff): Sequential(
(0): Linear(in_features=64, out_features=512, bias=True)
(1): GELUActivation()
(2): Identity()
(3): Linear(in_features=512, out_features=64, bias=True)
)
(dropout_path3): Identity()
(norm_sublayer3): PatchTSTBatchNorm(
(batchnorm): BatchNorm1d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
)
)
)
(head): PatchTSTPredictionHead(
(flatten): Flatten(start_dim=2, end_dim=-1)
(projection): Linear(in_features=64, out_features=12, bias=True)
(dropout): Identity()
)
)
df_TST = pd.read_csv('resources/BTCUSDT_1D.csv')
df_TST.head()
| Open Time | Close | |
|---|---|---|
| 0 | 2024-03-01 | 62387.90 |
| 1 | 2024-03-02 | 61987.28 |
| 2 | 2024-03-03 | 63113.97 |
| 3 | 2024-03-04 | 68245.71 |
| 4 | 2024-03-05 | 63724.01 |
# Parámetros
input_length = 24
forecast_steps = 40
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 1. Extraer valores de la serie
valores = df_TST.iloc[:, 1].values.astype(np.float32)
# 2. Normalizar la serie completa con el scaler ya entrenado
valores_norm = scaler.transform(valores.reshape(-1, 1)).flatten()
# 3. Crear ventanas deslizantes para los últimos forecast_steps puntos
serie_len = len(valores)
forecast_steps = min(forecast_steps, serie_len - input_length) # asegurar no exceder límite
start_idx = serie_len - forecast_steps - input_length
end_idx = serie_len - input_length
X = []
y_real = []
for i in range(start_idx, end_idx):
x_i = valores_norm[i : i + input_length]
y_i = valores[i + input_length]
X.append(x_i)
y_real.append(y_i)
X = np.array(X)
y_real = np.array(y_real)
# 4. Preparar tensores y predecir
X_tensor = torch.tensor(X, dtype=torch.float32).unsqueeze(-1).to(device)
model.eval()
with torch.no_grad():
output = model(X_tensor)
y_pred_norm = output.prediction_outputs[:, -1, 0].cpu().numpy() # último paso predicho
# 5. Desnormalizar predicciones
y_pred = scaler.inverse_transform(y_pred_norm.reshape(-1, 1)).flatten()
# 6. Calcular métricas
rmse_TST = root_mean_squared_error(y_real, y_pred)
mae_TST = mean_absolute_error(y_real, y_pred)
mape_TST = np.mean(np.abs((y_real - y_pred) / y_real)) * 100
metrics_dict_test = calculate_metrics(y_real, y_pred)
print("\nTest Set Metrics:")
for metric_name, metric_value in metrics_dict_test.items():
print(f"Score ({metric_name}): {metric_value}")
Test Set Metrics: Score (RMSE): 3566.8814109807463 Score (MAPE): 3.216271847486496 Score (MAE): 2764.9443359375 Score (MSE): 12722643.0 Score (sMAPE): 3.193509101867676 Score (RRMSE): 4.124702280485173 Score (Quantile Loss): 1382.47216796875
e:\Documentos\Git Repositories\uba-mia-ast2\.venv\Lib\site-packages\sklearn\utils\validation.py:2739: UserWarning: X does not have valid feature names, but StandardScaler was fitted with feature names warnings.warn(
plot_actual_vs_predicted(y_real, y_pred, "resources/patchtst_model_predict.jpeg")
metrics_dict_test_TST = {
"MAE": mae_TST,
"MAPE": mape_TST,
"RMSE": rmse_TST
}
df_line_TST = pd.DataFrame({
"model": "PatchTST",
"mae": metrics_dict_test_TST["MAE"],
"mape": metrics_dict_test_TST["MAPE"],
"rmse": metrics_dict_test_TST["RMSE"]
}, index=[0])
df_metrics = pd.concat([df_metrics, df_line_TST], ignore_index=True)
df_metrics
| model | mae | mape | rmse | |
|---|---|---|---|---|
| 0 | Naive | 1735.726500 | 1.888647 | 2434.504211 |
| 1 | ARIMA(1,1,35) | 1891.662281 | 2.063588 | 2546.257391 |
| 2 | ARIMA(0,1,0) | 1735.726500 | 1.894494 | 2434.504211 |
| 3 | Prophet | 2582.551322 | 2.871448 | 3621.703076 |
| 4 | XGBoost | 4319.031819 | 4.609317 | 5555.304516 |
| 5 | LSTM | 2432.965561 | 2.645095 | 3192.781096 |
| 6 | CustTransf | 2437.519884 | 2.837959 | 3324.778151 |
| 7 | CustTransfEmb | 2856.598098 | 3.335761 | 3681.151605 |
| 8 | TimesFM | 3279.778250 | 3.819422 | 3870.825711 |
| 9 | PatchTST | 2764.944336 | 3.216272 | 3566.881348 |
5. Resultados ¶
def plot_metrics_comparison_bar(df: pd.DataFrame, filename: str, fig_size = (10, 6)) -> None:
"""
Grafica una comparación de métricas (MAE) entre diferentes modelos,
ordenándolos de menor a mayor MAE, y guarda la figura en un archivo.
Args:
df (pd.DataFrame): DataFrame que contiene las métricas de los modelos.
filename (str): Nombre del archivo donde se guardará la figura.
fig_size (tuple): Tupla para el tamaño de la figura (ancho, alto).
Returns:
None
"""
df_grouped = df.groupby('model')['mae'].mean().reset_index()
df_sorted = df_grouped.sort_values(by='mae', ascending=True)
models = df_sorted['model'].tolist()
mae_values = df_sorted['mae'].tolist()
x_pos = np.arange(len(models))
plt.figure(figsize=fig_size, dpi=600)
plt.bar(x_pos, mae_values, color='skyblue')
plt.xlabel('Modelo')
plt.ylabel('Error Absoluto Medio (MAE)')
plt.title('Comparación de Métrica MAE por Modelo')
plt.xticks(x_pos, models, rotation=45, ha='right') # Rotar las etiquetas para que no se superpongan
# Añadir los valores de MAE encima de cada barra
for i, v in enumerate(mae_values):
plt.text(x_pos[i], v + 50, f"{v:.2f}", ha='center', va='bottom', fontsize=9)
plt.grid(axis='y', linestyle='--', alpha=0.3) # Añade una cuadrícula suave en el eje Y
plt.tight_layout() # Ajusta el diseño para que no se corten las etiquetas
plt.savefig(filename, format='jpeg', dpi=600)
plt.show()
def plot_metrics_radial(df: pd.DataFrame, filename: str, fig_size=(8, 8)) -> None:
"""
Grafica una comparación de múltiples métricas (MAE, MAPE, RMSE) para
diferentes modelos en un gráfico radial (o de araña/radar) y guarda la figura.
Las métricas se normalizan para una mejor comparación.
Args:
df (pd.DataFrame): DataFrame que contiene las métricas de los modelos.
filename (str): Nombre del archivo donde se guardará la figura.
fig_size (tuple): Tupla para el tamaño de la figura (ancho, alto).
Returns:
None
"""
metrics = ['mae', 'mape', 'rmse']
# Manejo de duplicados
df_processed = df.groupby('model')[metrics].mean().reset_index()
# Normalizar las métricas para que estén entre 0 y 1
df_normalized = df_processed.copy()
for metric in metrics:
min_val = df_processed[metric].min()
max_val = df_processed[metric].max()
# Evitar división por cero si todos los valores son iguales
if max_val == min_val:
df_normalized[metric] = 0.5 # Valor neutro si no hay variación
else:
# Invertir la escala para que "mejor" (menor error) esté más cerca del centro (0)
df_normalized[metric] = 1 - ((df_processed[metric] - min_val) / (max_val - min_val))
models = df_normalized['model'].tolist()
num_metrics = len(metrics)
# Calcular el ángulo para cada métrica
angles = np.linspace(0, 2 * np.pi, num_metrics, endpoint=False).tolist()
# Duplicar el primer ángulo y la primera métrica para cerrar el círculo del gráfico
angles += angles[:1]
# Configurar el gráfico
fig, ax = plt.subplots(figsize=fig_size, subplot_kw=dict(polar=True), dpi=600)
# Colores para cada modelo
colors = plt.colormaps.get_cmap('tab10')
# Graficar cada modelo
for i, model in enumerate(models):
values = df_normalized[df_normalized['model'] == model][metrics].values[0].tolist()
values += values[:1] # Cerrar el círculo para este modelo
ax.plot(angles, values, linewidth=2, linestyle='solid', label=model, color=colors(i))
ax.fill(angles, values, color=colors(i), alpha=0.25) # Rellenar el área
# Configurar las etiquetas de las métricas
ax.set_theta_offset(np.pi / 2) # Rotar el inicio a la parte superior
ax.set_theta_direction(-1) # Dirección en sentido horario
ax.set_xticks(angles[:-1])
ax.set_xticklabels(metrics)
# Configurar el eje radial
ax.set_yticks([0.2, 0.4, 0.6, 0.8, 1.0])
ax.set_yticklabels(['80%', '60%', '40%', '20%', '0% (Mejor)'], color='gray', size=8) # Etiquetas invertidas
ax.set_ylim(0, 1) # Rango del eje radial
# Título y leyenda
ax.set_title('Comparación de Modelos por Métricas', va='bottom', y=1.1)
ax.legend(loc='upper right', bbox_to_anchor=(1.3, 1.1))
plt.tight_layout()
plt.savefig(filename, format='jpeg', dpi=600)
plt.show()
plot_metrics_comparison_bar(df_metrics, "resources/metrics_comparison.jpeg")
plot_metrics_radial(df_metrics, "resources/metrics_radial.jpeg")
5.1 Conclusiones ¶
En este trabajo se exploró el uso de Transformers para series de tiempo, brindando una leve introducción a la arquitectura y su aplicación en el campo de series financieras.
Se implementaron dos modelos utilizando Keras, uno básico y otro con una codificación posicional alternativa, logrando resultados satisfactorios en la predicción de series temporales financieras (similares a modelos clásicos como LSTM o GRU).
También se exploró el uso de modelos pre-entrenados de HuggingFace, como TimesFM (originalmente de Google), que permite una mayor flexibilidad y adaptabilidad a diferentes series temporales, logrando resultados "interesantes" en la predicción de series temporales financieras.
Como conclusiones finales, se puede destacar que:
- 💡 El entrenamiento de los modelos "custom" desde cero fue más efectivo que el modelo preentrenado $\rightarrow$ Esto puede deberse a que
TimesFMfue entrenado con varios tipos de datos, pero ninguno relacionado a las series financiares. Sin dudas, es evidencia que para las series de tiempo, podría ser muy importante hacer fine-tuning de los modelos con varias series del dominio. - 💡 El uso de codificación posicional alternativa mejora la capacidad del modelo para capturar patrones temporales $\rightarrow$ Esto se debe a que la codificación posicional permite al modelo aprender patrones temporales de manera más efectiva, lo que mejora la precisión de las predicciones
- 💡 El uso de modelos pre-entrenados puede ser útil para tareas específicas, pero no siempre es la mejor opción $\rightarrow$ Esto se debe a que los modelos pre-entrenados pueden no estar adaptados a las características específicas de los datos, lo que puede afectar la precisión de las predicciones.
- 💡 El uso de Transformers para series de tiempo es una línea de investigación prometedora $\rightarrow$ Esto se debe a que los Transformers ofrecen ventajas significativas en términos de paralelización, atención y escalabilidad, lo que los hace adecuados para una amplia gama de tareas de series de tiempo, como se comentó al principio. Sin embargo, para este caso, sigue siendo el "mejor" modelo, simplemente predecir lo del día anterior.
- 💡 Clara diferencia de performance en series temporales a diferencia de NLP $\rightarrow$ Hay un clara diferencia de performance para estas arquitecturas en dominios del lenguaje natural con respecto al dominio de series temporales. Esto puede deberse a que en estos casos, donde los datos son escasos (solamente se intentó predecir con 1 año de datos), modelos con high inductive bias funcionan mejor que modelos con low inductive bias. Esto lo podemos evidenciar en que modelos clásicos como ARIMA han dado mejores resultados.
- 💡 El uso de Transformers para series de tiempo también requiere de muchos datos $\rightarrow$ Con los resultados, pudimos observar que se necesitan muchos más datos para que mejore la performance del modelo (relacionado al comentario anterior).
5.2 Mejoras y futuras lineas de investigación ¶
Entre las mejoras y futuras líneas de investigación, se pueden destacar las siguientes:
- 💫 Optimización de parámetros y arquitectura: Sin duda un punto fuerte de este trabajo la no optimización de la arquitectura e hiperparámetros. Esto se debe a una restricción temporal, pero sería interesante realizar este proceso para los modelos "custom".
- 💫 Realizar _fine-tuning_ sobre el modelo
TimesFM: Creemos que es muy importante para estos casos que los modelos estén pre-entrenados con datos relacionados. Es por esto, que se sugiere probar realizar fine-tuning sobre este modelo con series financieras similares. - 💫 Probar con otros modelos pre-entrenados: Existen otros modelos pre-entrenados que podrían ser útiles para este caso, como
LogTransoInformer, que podrían mejorar la precisión de las predicciones. - 💫 Aumentar la cantidad de datos: Sin dudas mejoraría la performance de estas arquitecturas tener más datos, como en NLP. En este ejemplo se evidenció que el Transformer no logra generalizar correctamente con esta cantidad de datos.
- 💫 Explorar otras características y embeddings: En este caso, se mejoró con la incorporación de embeddings. Esto podría dar lugar a explorar esta linea de mejor generación de embeddings como incorporar más características a la serie.
- 💫 Tamaño de ventana: En esta exploración se utilizó simplemente un tamaño de ventana chico para continuar con el análisis previo pero se podría incluir en la optimización de hiperparámetros o investigar más su inferencia.
- 💫 Serie univariable en PatchTST: La forma de implementar PatchTST en el ejemplo no es del todo alineada con el paper, dado que este propone utilizar varias series temporales y en este caso se utilizó solamente una (la de Bitcoin). Pero lo ideal sería realizar un análisis con varias series en varios canales.
6. Referencias ¶
- Transformers in time series: A survey | https://arxiv.org/abs/2202.07125
- Transformer models: an introduction and catalog | https://arxiv.org/abs/2302.07730
- Enhancing the Locality and Breaking the Memory Bottleneck of Transformer on Time Series Forecasting (2020) | https://arxiv.org/abs/1907.00235
- How to apply transformers to time series models | https://medium.com/intel-tech/how-to-apply-transformers-to-time-series-models-spacetimeformer-e452f2825d2e
- A Time Series is Worth 64 Words: Long-term Forecasting with Transformers (2023) | https://arxiv.org/abs/2211.14730
- A decoder-only foundation model for time-series forecasting | https://huggingface.co/papers/2310.10688
- LogTrans: Providing Efficient Local-Global Fusion with Transformer and CNN Parallel Network for Biomedical Image Segmentation | https://ieeexplore.ieee.org/document/10074688
- Pyraformer: Low-Complexity Pyramidal Attention for Long-Range Time Series Modeling and Forecasting | https://openreview.net/forum?id=0EXmFzUn5I
- FEDformer: Frequency Enhanced Decomposed Transformer for Long-term Series Forecasting | https://arxiv.org/abs/2201.12740
- Is my take on transformers in time series reasonable / where is it wrong? |https://www.reddit.com/r/MachineLearning/comments/1k63r4a/d_is_my_take_on_transformers_in_time_series/